From 01fae4e10f4c1a76fea0add67cf8fab3a187e9b4 Mon Sep 17 00:00:00 2001 From: benny Date: Tue, 18 Nov 2025 20:18:33 +0000 Subject: [PATCH] Initial import of NetDeploy project --- .gitignore | 24 + app.py | 397 + netdeploy/bin/Activate.ps1 | 247 + netdeploy/bin/activate | 69 + netdeploy/bin/activate.csh | 26 + netdeploy/bin/activate.fish | 69 + netdeploy/bin/dotenv | 8 + netdeploy/bin/flask | 8 + netdeploy/bin/gunicorn | 8 + netdeploy/bin/pip | 8 + netdeploy/bin/pip3 | 8 + netdeploy/bin/pip3.11 | 8 + netdeploy/bin/python | 1 + netdeploy/bin/python3 | 1 + netdeploy/bin/python3.11 | 1 + .../site/python3.11/greenlet/greenlet.h | 164 + .../site-packages/_distutils_hack/__init__.py | 222 + .../site-packages/_distutils_hack/override.py | 1 + .../blinker-1.9.0.dist-info/INSTALLER | 1 + .../blinker-1.9.0.dist-info/LICENSE.txt | 20 + .../blinker-1.9.0.dist-info/METADATA | 60 + .../blinker-1.9.0.dist-info/RECORD | 12 + .../blinker-1.9.0.dist-info/WHEEL | 4 + .../site-packages/blinker/__init__.py | 17 + .../site-packages/blinker/_utilities.py | 64 + .../python3.11/site-packages/blinker/base.py | 512 + .../python3.11/site-packages/blinker/py.typed | 0 .../click-8.3.0.dist-info/INSTALLER | 1 + .../click-8.3.0.dist-info/METADATA | 84 + .../click-8.3.0.dist-info/RECORD | 40 + .../site-packages/click-8.3.0.dist-info/WHEEL | 4 + .../licenses/LICENSE.txt | 28 + .../site-packages/click/__init__.py | 123 + .../python3.11/site-packages/click/_compat.py | 622 ++ .../site-packages/click/_termui_impl.py | 847 ++ .../site-packages/click/_textwrap.py | 51 + .../python3.11/site-packages/click/_utils.py | 36 + .../site-packages/click/_winconsole.py | 296 + .../python3.11/site-packages/click/core.py | 3347 +++++++ .../site-packages/click/decorators.py | 551 ++ .../site-packages/click/exceptions.py | 308 + .../site-packages/click/formatting.py | 301 + .../python3.11/site-packages/click/globals.py | 67 + .../python3.11/site-packages/click/parser.py | 532 + .../python3.11/site-packages/click/py.typed | 0 .../site-packages/click/shell_completion.py | 667 ++ .../python3.11/site-packages/click/termui.py | 877 ++ .../python3.11/site-packages/click/testing.py | 577 ++ .../python3.11/site-packages/click/types.py | 1209 +++ .../python3.11/site-packages/click/utils.py | 627 ++ .../site-packages/distutils-precedence.pth | 1 + .../python3.11/site-packages/dns/__init__.py | 72 + .../site-packages/dns/_asyncbackend.py | 100 + .../site-packages/dns/_asyncio_backend.py | 276 + .../lib/python3.11/site-packages/dns/_ddr.py | 154 + .../python3.11/site-packages/dns/_features.py | 95 + .../site-packages/dns/_immutable_ctx.py | 76 + .../python3.11/site-packages/dns/_no_ssl.py | 61 + .../python3.11/site-packages/dns/_tls_util.py | 19 + .../site-packages/dns/_trio_backend.py | 255 + .../site-packages/dns/asyncbackend.py | 101 + .../site-packages/dns/asyncquery.py | 953 ++ .../site-packages/dns/asyncresolver.py | 478 + .../lib/python3.11/site-packages/dns/btree.py | 850 ++ .../python3.11/site-packages/dns/btreezone.py | 367 + .../python3.11/site-packages/dns/dnssec.py | 1242 +++ .../site-packages/dns/dnssecalgs/__init__.py | 124 + .../site-packages/dns/dnssecalgs/base.py | 89 + .../dns/dnssecalgs/cryptography.py | 68 + .../site-packages/dns/dnssecalgs/dsa.py | 108 + .../site-packages/dns/dnssecalgs/ecdsa.py | 100 + .../site-packages/dns/dnssecalgs/eddsa.py | 70 + .../site-packages/dns/dnssecalgs/rsa.py | 126 + .../site-packages/dns/dnssectypes.py | 71 + .../lib/python3.11/site-packages/dns/e164.py | 116 + .../lib/python3.11/site-packages/dns/edns.py | 591 ++ .../python3.11/site-packages/dns/entropy.py | 130 + .../lib/python3.11/site-packages/dns/enum.py | 113 + .../python3.11/site-packages/dns/exception.py | 169 + .../lib/python3.11/site-packages/dns/flags.py | 123 + .../python3.11/site-packages/dns/grange.py | 72 + .../python3.11/site-packages/dns/immutable.py | 68 + .../lib/python3.11/site-packages/dns/inet.py | 195 + .../lib/python3.11/site-packages/dns/ipv4.py | 76 + .../lib/python3.11/site-packages/dns/ipv6.py | 217 + .../python3.11/site-packages/dns/message.py | 1954 ++++ .../lib/python3.11/site-packages/dns/name.py | 1289 +++ .../python3.11/site-packages/dns/namedict.py | 109 + .../site-packages/dns/nameserver.py | 361 + .../lib/python3.11/site-packages/dns/node.py | 358 + .../python3.11/site-packages/dns/opcode.py | 119 + .../lib/python3.11/site-packages/dns/py.typed | 0 .../lib/python3.11/site-packages/dns/query.py | 1786 ++++ .../site-packages/dns/quic/__init__.py | 78 + .../site-packages/dns/quic/_asyncio.py | 276 + .../site-packages/dns/quic/_common.py | 344 + .../site-packages/dns/quic/_sync.py | 306 + .../site-packages/dns/quic/_trio.py | 250 + .../lib/python3.11/site-packages/dns/rcode.py | 168 + .../lib/python3.11/site-packages/dns/rdata.py | 935 ++ .../site-packages/dns/rdataclass.py | 118 + .../python3.11/site-packages/dns/rdataset.py | 508 + .../python3.11/site-packages/dns/rdatatype.py | 338 + .../site-packages/dns/rdtypes/ANY/AFSDB.py | 45 + .../site-packages/dns/rdtypes/ANY/AMTRELAY.py | 89 + .../site-packages/dns/rdtypes/ANY/AVC.py | 26 + .../site-packages/dns/rdtypes/ANY/CAA.py | 67 + .../site-packages/dns/rdtypes/ANY/CDNSKEY.py | 33 + .../site-packages/dns/rdtypes/ANY/CDS.py | 29 + .../site-packages/dns/rdtypes/ANY/CERT.py | 113 + .../site-packages/dns/rdtypes/ANY/CNAME.py | 28 + .../site-packages/dns/rdtypes/ANY/CSYNC.py | 68 + .../site-packages/dns/rdtypes/ANY/DLV.py | 24 + .../site-packages/dns/rdtypes/ANY/DNAME.py | 27 + .../site-packages/dns/rdtypes/ANY/DNSKEY.py | 33 + .../site-packages/dns/rdtypes/ANY/DS.py | 24 + .../site-packages/dns/rdtypes/ANY/DSYNC.py | 72 + .../site-packages/dns/rdtypes/ANY/EUI48.py | 30 + .../site-packages/dns/rdtypes/ANY/EUI64.py | 30 + .../site-packages/dns/rdtypes/ANY/GPOS.py | 126 + .../site-packages/dns/rdtypes/ANY/HINFO.py | 64 + .../site-packages/dns/rdtypes/ANY/HIP.py | 85 + .../site-packages/dns/rdtypes/ANY/ISDN.py | 78 + .../site-packages/dns/rdtypes/ANY/L32.py | 42 + .../site-packages/dns/rdtypes/ANY/L64.py | 48 + .../site-packages/dns/rdtypes/ANY/LOC.py | 347 + .../site-packages/dns/rdtypes/ANY/LP.py | 42 + .../site-packages/dns/rdtypes/ANY/MX.py | 24 + .../site-packages/dns/rdtypes/ANY/NID.py | 48 + .../site-packages/dns/rdtypes/ANY/NINFO.py | 26 + .../site-packages/dns/rdtypes/ANY/NS.py | 24 + .../site-packages/dns/rdtypes/ANY/NSEC.py | 67 + .../site-packages/dns/rdtypes/ANY/NSEC3.py | 120 + .../dns/rdtypes/ANY/NSEC3PARAM.py | 69 + .../dns/rdtypes/ANY/OPENPGPKEY.py | 53 + .../site-packages/dns/rdtypes/ANY/OPT.py | 77 + .../site-packages/dns/rdtypes/ANY/PTR.py | 24 + .../site-packages/dns/rdtypes/ANY/RESINFO.py | 24 + .../site-packages/dns/rdtypes/ANY/RP.py | 58 + .../site-packages/dns/rdtypes/ANY/RRSIG.py | 155 + .../site-packages/dns/rdtypes/ANY/RT.py | 24 + .../site-packages/dns/rdtypes/ANY/SMIMEA.py | 9 + .../site-packages/dns/rdtypes/ANY/SOA.py | 78 + .../site-packages/dns/rdtypes/ANY/SPF.py | 26 + .../site-packages/dns/rdtypes/ANY/SSHFP.py | 67 + .../site-packages/dns/rdtypes/ANY/TKEY.py | 135 + .../site-packages/dns/rdtypes/ANY/TLSA.py | 9 + .../site-packages/dns/rdtypes/ANY/TSIG.py | 160 + .../site-packages/dns/rdtypes/ANY/TXT.py | 24 + .../site-packages/dns/rdtypes/ANY/URI.py | 79 + .../site-packages/dns/rdtypes/ANY/WALLET.py | 9 + .../site-packages/dns/rdtypes/ANY/X25.py | 57 + .../site-packages/dns/rdtypes/ANY/ZONEMD.py | 64 + .../site-packages/dns/rdtypes/ANY/__init__.py | 71 + .../site-packages/dns/rdtypes/CH/A.py | 60 + .../site-packages/dns/rdtypes/CH/__init__.py | 22 + .../site-packages/dns/rdtypes/IN/A.py | 51 + .../site-packages/dns/rdtypes/IN/AAAA.py | 51 + .../site-packages/dns/rdtypes/IN/APL.py | 150 + .../site-packages/dns/rdtypes/IN/DHCID.py | 54 + .../site-packages/dns/rdtypes/IN/HTTPS.py | 9 + .../site-packages/dns/rdtypes/IN/IPSECKEY.py | 87 + .../site-packages/dns/rdtypes/IN/KX.py | 24 + .../site-packages/dns/rdtypes/IN/NAPTR.py | 109 + .../site-packages/dns/rdtypes/IN/NSAP.py | 60 + .../site-packages/dns/rdtypes/IN/NSAP_PTR.py | 24 + .../site-packages/dns/rdtypes/IN/PX.py | 73 + .../site-packages/dns/rdtypes/IN/SRV.py | 75 + .../site-packages/dns/rdtypes/IN/SVCB.py | 9 + .../site-packages/dns/rdtypes/IN/WKS.py | 100 + .../site-packages/dns/rdtypes/IN/__init__.py | 35 + .../site-packages/dns/rdtypes/__init__.py | 33 + .../site-packages/dns/rdtypes/dnskeybase.py | 83 + .../site-packages/dns/rdtypes/dsbase.py | 83 + .../site-packages/dns/rdtypes/euibase.py | 73 + .../site-packages/dns/rdtypes/mxbase.py | 87 + .../site-packages/dns/rdtypes/nsbase.py | 63 + .../site-packages/dns/rdtypes/svcbbase.py | 587 ++ .../site-packages/dns/rdtypes/tlsabase.py | 69 + .../site-packages/dns/rdtypes/txtbase.py | 109 + .../site-packages/dns/rdtypes/util.py | 269 + .../python3.11/site-packages/dns/renderer.py | 355 + .../python3.11/site-packages/dns/resolver.py | 2068 ++++ .../site-packages/dns/reversename.py | 106 + .../lib/python3.11/site-packages/dns/rrset.py | 287 + .../python3.11/site-packages/dns/serial.py | 118 + .../lib/python3.11/site-packages/dns/set.py | 308 + .../python3.11/site-packages/dns/tokenizer.py | 706 ++ .../site-packages/dns/transaction.py | 651 ++ .../lib/python3.11/site-packages/dns/tsig.py | 359 + .../site-packages/dns/tsigkeyring.py | 68 + .../lib/python3.11/site-packages/dns/ttl.py | 90 + .../python3.11/site-packages/dns/update.py | 389 + .../python3.11/site-packages/dns/version.py | 42 + .../python3.11/site-packages/dns/versioned.py | 320 + .../python3.11/site-packages/dns/win32util.py | 438 + .../lib/python3.11/site-packages/dns/wire.py | 98 + .../lib/python3.11/site-packages/dns/xfr.py | 356 + .../lib/python3.11/site-packages/dns/zone.py | 1462 +++ .../python3.11/site-packages/dns/zonefile.py | 756 ++ .../python3.11/site-packages/dns/zonetypes.py | 37 + .../dnspython-2.8.0.dist-info/INSTALLER | 1 + .../dnspython-2.8.0.dist-info/METADATA | 149 + .../dnspython-2.8.0.dist-info/RECORD | 304 + .../dnspython-2.8.0.dist-info/WHEEL | 4 + .../licenses/LICENSE | 35 + .../site-packages/dotenv/__init__.py | 49 + .../site-packages/dotenv/__main__.py | 6 + .../python3.11/site-packages/dotenv/cli.py | 199 + .../site-packages/dotenv/ipython.py | 39 + .../python3.11/site-packages/dotenv/main.py | 392 + .../python3.11/site-packages/dotenv/parser.py | 175 + .../python3.11/site-packages/dotenv/py.typed | 1 + .../site-packages/dotenv/variables.py | 86 + .../site-packages/dotenv/version.py | 1 + .../eventlet-0.40.3.dist-info/INSTALLER | 1 + .../eventlet-0.40.3.dist-info/METADATA | 129 + .../eventlet-0.40.3.dist-info/RECORD | 199 + .../eventlet-0.40.3.dist-info/REQUESTED | 0 .../eventlet-0.40.3.dist-info/WHEEL | 4 + .../licenses/AUTHORS | 189 + .../licenses/LICENSE | 23 + .../site-packages/eventlet/__init__.py | 88 + .../site-packages/eventlet/_version.py | 34 + .../site-packages/eventlet/asyncio.py | 57 + .../site-packages/eventlet/backdoor.py | 140 + .../site-packages/eventlet/convenience.py | 190 + .../site-packages/eventlet/corolocal.py | 53 + .../site-packages/eventlet/coros.py | 59 + .../site-packages/eventlet/dagpool.py | 601 ++ .../site-packages/eventlet/db_pool.py | 460 + .../site-packages/eventlet/debug.py | 222 + .../site-packages/eventlet/event.py | 218 + .../eventlet/green/BaseHTTPServer.py | 15 + .../eventlet/green/CGIHTTPServer.py | 17 + .../site-packages/eventlet/green/MySQLdb.py | 40 + .../eventlet/green/OpenSSL/SSL.py | 125 + .../eventlet/green/OpenSSL/__init__.py | 9 + .../eventlet/green/OpenSSL/crypto.py | 1 + .../eventlet/green/OpenSSL/tsafe.py | 1 + .../eventlet/green/OpenSSL/version.py | 1 + .../site-packages/eventlet/green/Queue.py | 33 + .../eventlet/green/SimpleHTTPServer.py | 13 + .../eventlet/green/SocketServer.py | 14 + .../site-packages/eventlet/green/__init__.py | 1 + .../eventlet/green/_socket_nodns.py | 33 + .../site-packages/eventlet/green/asynchat.py | 14 + .../site-packages/eventlet/green/asyncore.py | 16 + .../site-packages/eventlet/green/builtin.py | 38 + .../site-packages/eventlet/green/ftplib.py | 13 + .../eventlet/green/http/__init__.py | 189 + .../eventlet/green/http/client.py | 1578 +++ .../eventlet/green/http/cookiejar.py | 2154 ++++ .../eventlet/green/http/cookies.py | 691 ++ .../eventlet/green/http/server.py | 1266 +++ .../site-packages/eventlet/green/httplib.py | 18 + .../site-packages/eventlet/green/os.py | 133 + .../site-packages/eventlet/green/profile.py | 257 + .../site-packages/eventlet/green/select.py | 86 + .../site-packages/eventlet/green/selectors.py | 34 + .../site-packages/eventlet/green/socket.py | 63 + .../site-packages/eventlet/green/ssl.py | 487 + .../eventlet/green/subprocess.py | 137 + .../site-packages/eventlet/green/thread.py | 178 + .../site-packages/eventlet/green/threading.py | 133 + .../site-packages/eventlet/green/time.py | 6 + .../eventlet/green/urllib/__init__.py | 5 + .../eventlet/green/urllib/error.py | 4 + .../eventlet/green/urllib/parse.py | 3 + .../eventlet/green/urllib/request.py | 57 + .../eventlet/green/urllib/response.py | 3 + .../site-packages/eventlet/green/urllib2.py | 20 + .../site-packages/eventlet/green/zmq.py | 465 + .../eventlet/greenio/__init__.py | 3 + .../site-packages/eventlet/greenio/base.py | 485 + .../site-packages/eventlet/greenio/py3.py | 227 + .../site-packages/eventlet/greenpool.py | 254 + .../site-packages/eventlet/greenthread.py | 353 + .../site-packages/eventlet/hubs/__init__.py | 188 + .../site-packages/eventlet/hubs/asyncio.py | 174 + .../site-packages/eventlet/hubs/epolls.py | 31 + .../site-packages/eventlet/hubs/hub.py | 495 + .../site-packages/eventlet/hubs/kqueue.py | 110 + .../site-packages/eventlet/hubs/poll.py | 118 + .../site-packages/eventlet/hubs/pyevent.py | 4 + .../site-packages/eventlet/hubs/selects.py | 63 + .../site-packages/eventlet/hubs/timer.py | 106 + .../python3.11/site-packages/eventlet/lock.py | 37 + .../site-packages/eventlet/patcher.py | 773 ++ .../site-packages/eventlet/pools.py | 184 + .../site-packages/eventlet/queue.py | 496 + .../site-packages/eventlet/semaphore.py | 315 + .../eventlet/support/__init__.py | 69 + .../eventlet/support/greendns.py | 959 ++ .../eventlet/support/greenlets.py | 4 + .../eventlet/support/psycopg2_patcher.py | 55 + .../site-packages/eventlet/support/pylib.py | 12 + .../eventlet/support/stacklesspypys.py | 12 + .../eventlet/support/stacklesss.py | 84 + .../site-packages/eventlet/timeout.py | 184 + .../site-packages/eventlet/tpool.py | 336 + .../site-packages/eventlet/websocket.py | 868 ++ .../python3.11/site-packages/eventlet/wsgi.py | 1102 +++ .../site-packages/eventlet/zipkin/README.rst | 130 + .../site-packages/eventlet/zipkin/__init__.py | 0 .../eventlet/zipkin/_thrift/README.rst | 8 + .../eventlet/zipkin/_thrift/__init__.py | 0 .../eventlet/zipkin/_thrift/zipkinCore.thrift | 55 + .../zipkin/_thrift/zipkinCore/__init__.py | 1 + .../zipkin/_thrift/zipkinCore/constants.py | 14 + .../zipkin/_thrift/zipkinCore/ttypes.py | 452 + .../site-packages/eventlet/zipkin/api.py | 187 + .../site-packages/eventlet/zipkin/client.py | 56 + .../eventlet/zipkin/example/ex1.png | Bin 0 -> 53179 bytes .../eventlet/zipkin/example/ex2.png | Bin 0 -> 40482 bytes .../eventlet/zipkin/example/ex3.png | Bin 0 -> 73175 bytes .../eventlet/zipkin/greenthread.py | 33 + .../site-packages/eventlet/zipkin/http.py | 29 + .../site-packages/eventlet/zipkin/log.py | 19 + .../site-packages/eventlet/zipkin/patcher.py | 41 + .../site-packages/eventlet/zipkin/wsgi.py | 78 + .../flask-3.0.3.dist-info/INSTALLER | 1 + .../flask-3.0.3.dist-info/LICENSE.txt | 28 + .../flask-3.0.3.dist-info/METADATA | 101 + .../flask-3.0.3.dist-info/RECORD | 58 + .../flask-3.0.3.dist-info/REQUESTED | 0 .../site-packages/flask-3.0.3.dist-info/WHEEL | 4 + .../flask-3.0.3.dist-info/entry_points.txt | 3 + .../site-packages/flask/__init__.py | 60 + .../site-packages/flask/__main__.py | 3 + .../lib/python3.11/site-packages/flask/app.py | 1498 +++ .../site-packages/flask/blueprints.py | 129 + .../lib/python3.11/site-packages/flask/cli.py | 1109 +++ .../python3.11/site-packages/flask/config.py | 370 + .../lib/python3.11/site-packages/flask/ctx.py | 449 + .../site-packages/flask/debughelpers.py | 178 + .../python3.11/site-packages/flask/globals.py | 51 + .../python3.11/site-packages/flask/helpers.py | 621 ++ .../site-packages/flask/json/__init__.py | 170 + .../site-packages/flask/json/provider.py | 215 + .../site-packages/flask/json/tag.py | 327 + .../python3.11/site-packages/flask/logging.py | 79 + .../python3.11/site-packages/flask/py.typed | 0 .../site-packages/flask/sansio/README.md | 6 + .../site-packages/flask/sansio/app.py | 964 ++ .../site-packages/flask/sansio/blueprints.py | 632 ++ .../site-packages/flask/sansio/scaffold.py | 801 ++ .../site-packages/flask/sessions.py | 379 + .../python3.11/site-packages/flask/signals.py | 17 + .../site-packages/flask/templating.py | 219 + .../python3.11/site-packages/flask/testing.py | 298 + .../python3.11/site-packages/flask/typing.py | 90 + .../python3.11/site-packages/flask/views.py | 191 + .../site-packages/flask/wrappers.py | 174 + .../greenlet-3.2.4.dist-info/INSTALLER | 1 + .../greenlet-3.2.4.dist-info/METADATA | 117 + .../greenlet-3.2.4.dist-info/RECORD | 121 + .../greenlet-3.2.4.dist-info/WHEEL | 6 + .../greenlet-3.2.4.dist-info/licenses/LICENSE | 30 + .../licenses/LICENSE.PSF | 47 + .../greenlet-3.2.4.dist-info/top_level.txt | 1 + .../site-packages/greenlet/CObjects.cpp | 157 + .../site-packages/greenlet/PyGreenlet.cpp | 751 ++ .../site-packages/greenlet/PyGreenlet.hpp | 35 + .../greenlet/PyGreenletUnswitchable.cpp | 147 + .../site-packages/greenlet/PyModule.cpp | 292 + .../greenlet/TBrokenGreenlet.cpp | 45 + .../greenlet/TExceptionState.cpp | 62 + .../site-packages/greenlet/TGreenlet.cpp | 719 ++ .../site-packages/greenlet/TGreenlet.hpp | 830 ++ .../greenlet/TGreenletGlobals.cpp | 94 + .../site-packages/greenlet/TMainGreenlet.cpp | 153 + .../site-packages/greenlet/TPythonState.cpp | 406 + .../site-packages/greenlet/TStackState.cpp | 265 + .../site-packages/greenlet/TThreadState.hpp | 497 + .../greenlet/TThreadStateCreator.hpp | 102 + .../greenlet/TThreadStateDestroy.cpp | 217 + .../site-packages/greenlet/TUserGreenlet.cpp | 662 ++ .../site-packages/greenlet/__init__.py | 71 + .../_greenlet.cpython-311-x86_64-linux-gnu.so | Bin 0 -> 1365232 bytes .../site-packages/greenlet/greenlet.cpp | 320 + .../site-packages/greenlet/greenlet.h | 164 + .../greenlet/greenlet_allocator.hpp | 89 + .../greenlet/greenlet_compiler_compat.hpp | 98 + .../greenlet/greenlet_cpython_compat.hpp | 150 + .../greenlet/greenlet_exceptions.hpp | 171 + .../greenlet/greenlet_internal.hpp | 107 + .../greenlet/greenlet_msvc_compat.hpp | 91 + .../site-packages/greenlet/greenlet_refs.hpp | 1118 +++ .../greenlet/greenlet_slp_switch.hpp | 99 + .../greenlet/greenlet_thread_support.hpp | 31 + .../greenlet/platform/__init__.py | 0 .../platform/setup_switch_x64_masm.cmd | 2 + .../greenlet/platform/switch_aarch64_gcc.h | 124 + .../greenlet/platform/switch_alpha_unix.h | 30 + .../greenlet/platform/switch_amd64_unix.h | 87 + .../greenlet/platform/switch_arm32_gcc.h | 79 + .../greenlet/platform/switch_arm32_ios.h | 67 + .../greenlet/platform/switch_arm64_masm.asm | 53 + .../greenlet/platform/switch_arm64_masm.obj | Bin 0 -> 746 bytes .../greenlet/platform/switch_arm64_msvc.h | 17 + .../greenlet/platform/switch_csky_gcc.h | 48 + .../platform/switch_loongarch64_linux.h | 31 + .../greenlet/platform/switch_m68k_gcc.h | 38 + .../greenlet/platform/switch_mips_unix.h | 64 + .../greenlet/platform/switch_ppc64_aix.h | 103 + .../greenlet/platform/switch_ppc64_linux.h | 105 + .../greenlet/platform/switch_ppc_aix.h | 87 + .../greenlet/platform/switch_ppc_linux.h | 84 + .../greenlet/platform/switch_ppc_macosx.h | 82 + .../greenlet/platform/switch_ppc_unix.h | 82 + .../greenlet/platform/switch_riscv_unix.h | 41 + .../greenlet/platform/switch_s390_unix.h | 87 + .../greenlet/platform/switch_sh_gcc.h | 36 + .../greenlet/platform/switch_sparc_sun_gcc.h | 92 + .../greenlet/platform/switch_x32_unix.h | 63 + .../greenlet/platform/switch_x64_masm.asm | 111 + .../greenlet/platform/switch_x64_masm.obj | Bin 0 -> 1078 bytes .../greenlet/platform/switch_x64_msvc.h | 60 + .../greenlet/platform/switch_x86_msvc.h | 326 + .../greenlet/platform/switch_x86_unix.h | 105 + .../greenlet/slp_platformselect.h | 77 + .../site-packages/greenlet/tests/__init__.py | 248 + .../greenlet/tests/_test_extension.c | 231 + ..._extension.cpython-311-x86_64-linux-gnu.so | Bin 0 -> 17256 bytes .../greenlet/tests/_test_extension_cpp.cpp | 226 + ...ension_cpp.cpython-311-x86_64-linux-gnu.so | Bin 0 -> 57920 bytes .../tests/fail_clearing_run_switches.py | 47 + .../greenlet/tests/fail_cpp_exception.py | 33 + .../tests/fail_initialstub_already_started.py | 78 + .../greenlet/tests/fail_slp_switch.py | 29 + .../tests/fail_switch_three_greenlets.py | 44 + .../tests/fail_switch_three_greenlets2.py | 55 + .../tests/fail_switch_two_greenlets.py | 41 + .../site-packages/greenlet/tests/leakcheck.py | 336 + .../greenlet/tests/test_contextvars.py | 312 + .../site-packages/greenlet/tests/test_cpp.py | 73 + .../tests/test_extension_interface.py | 115 + .../site-packages/greenlet/tests/test_gc.py | 86 + .../greenlet/tests/test_generator.py | 59 + .../greenlet/tests/test_generator_nested.py | 168 + .../greenlet/tests/test_greenlet.py | 1353 +++ .../greenlet/tests/test_greenlet_trash.py | 187 + .../greenlet/tests/test_leaks.py | 457 + .../greenlet/tests/test_stack_saved.py | 19 + .../greenlet/tests/test_throw.py | 128 + .../greenlet/tests/test_tracing.py | 299 + .../greenlet/tests/test_version.py | 41 + .../greenlet/tests/test_weakref.py | 35 + .../gunicorn-23.0.0.dist-info/INSTALLER | 1 + .../gunicorn-23.0.0.dist-info/LICENSE | 23 + .../gunicorn-23.0.0.dist-info/METADATA | 130 + .../gunicorn-23.0.0.dist-info/RECORD | 77 + .../gunicorn-23.0.0.dist-info/REQUESTED | 0 .../gunicorn-23.0.0.dist-info/WHEEL | 5 + .../entry_points.txt | 5 + .../gunicorn-23.0.0.dist-info/top_level.txt | 1 + .../site-packages/gunicorn/__init__.py | 8 + .../site-packages/gunicorn/__main__.py | 10 + .../site-packages/gunicorn/app/__init__.py | 3 + .../site-packages/gunicorn/app/base.py | 235 + .../site-packages/gunicorn/app/pasterapp.py | 74 + .../site-packages/gunicorn/app/wsgiapp.py | 70 + .../site-packages/gunicorn/arbiter.py | 671 ++ .../site-packages/gunicorn/config.py | 2442 +++++ .../site-packages/gunicorn/debug.py | 68 + .../site-packages/gunicorn/errors.py | 28 + .../site-packages/gunicorn/glogging.py | 473 + .../site-packages/gunicorn/http/__init__.py | 8 + .../site-packages/gunicorn/http/body.py | 268 + .../site-packages/gunicorn/http/errors.py | 145 + .../site-packages/gunicorn/http/message.py | 463 + .../site-packages/gunicorn/http/parser.py | 51 + .../site-packages/gunicorn/http/unreader.py | 78 + .../site-packages/gunicorn/http/wsgi.py | 401 + .../gunicorn/instrument/__init__.py | 0 .../gunicorn/instrument/statsd.py | 134 + .../site-packages/gunicorn/pidfile.py | 85 + .../site-packages/gunicorn/reloader.py | 131 + .../python3.11/site-packages/gunicorn/sock.py | 231 + .../site-packages/gunicorn/systemd.py | 75 + .../python3.11/site-packages/gunicorn/util.py | 653 ++ .../gunicorn/workers/__init__.py | 14 + .../site-packages/gunicorn/workers/base.py | 287 + .../gunicorn/workers/base_async.py | 147 + .../gunicorn/workers/geventlet.py | 186 + .../site-packages/gunicorn/workers/ggevent.py | 193 + .../site-packages/gunicorn/workers/gthread.py | 372 + .../gunicorn/workers/gtornado.py | 166 + .../site-packages/gunicorn/workers/sync.py | 209 + .../gunicorn/workers/workertmp.py | 53 + .../itsdangerous-2.2.0.dist-info/INSTALLER | 1 + .../itsdangerous-2.2.0.dist-info/LICENSE.txt | 28 + .../itsdangerous-2.2.0.dist-info/METADATA | 60 + .../itsdangerous-2.2.0.dist-info/RECORD | 22 + .../itsdangerous-2.2.0.dist-info/WHEEL | 4 + .../site-packages/itsdangerous/__init__.py | 38 + .../site-packages/itsdangerous/_json.py | 18 + .../site-packages/itsdangerous/encoding.py | 54 + .../site-packages/itsdangerous/exc.py | 106 + .../site-packages/itsdangerous/py.typed | 0 .../site-packages/itsdangerous/serializer.py | 406 + .../site-packages/itsdangerous/signer.py | 266 + .../site-packages/itsdangerous/timed.py | 228 + .../site-packages/itsdangerous/url_safe.py | 83 + .../jinja2-3.1.6.dist-info/INSTALLER | 1 + .../jinja2-3.1.6.dist-info/METADATA | 84 + .../jinja2-3.1.6.dist-info/RECORD | 57 + .../jinja2-3.1.6.dist-info/WHEEL | 4 + .../jinja2-3.1.6.dist-info/entry_points.txt | 3 + .../licenses/LICENSE.txt | 28 + .../site-packages/jinja2/__init__.py | 38 + .../site-packages/jinja2/_identifier.py | 6 + .../site-packages/jinja2/async_utils.py | 99 + .../site-packages/jinja2/bccache.py | 408 + .../site-packages/jinja2/compiler.py | 1998 ++++ .../site-packages/jinja2/constants.py | 20 + .../python3.11/site-packages/jinja2/debug.py | 191 + .../site-packages/jinja2/defaults.py | 48 + .../site-packages/jinja2/environment.py | 1672 ++++ .../site-packages/jinja2/exceptions.py | 166 + .../python3.11/site-packages/jinja2/ext.py | 870 ++ .../site-packages/jinja2/filters.py | 1873 ++++ .../site-packages/jinja2/idtracking.py | 318 + .../python3.11/site-packages/jinja2/lexer.py | 868 ++ .../site-packages/jinja2/loaders.py | 693 ++ .../python3.11/site-packages/jinja2/meta.py | 112 + .../site-packages/jinja2/nativetypes.py | 130 + .../python3.11/site-packages/jinja2/nodes.py | 1206 +++ .../site-packages/jinja2/optimizer.py | 48 + .../python3.11/site-packages/jinja2/parser.py | 1049 ++ .../python3.11/site-packages/jinja2/py.typed | 0 .../site-packages/jinja2/runtime.py | 1062 ++ .../site-packages/jinja2/sandbox.py | 436 + .../python3.11/site-packages/jinja2/tests.py | 256 + .../python3.11/site-packages/jinja2/utils.py | 766 ++ .../site-packages/jinja2/visitor.py | 92 + .../markupsafe-3.0.3.dist-info/INSTALLER | 1 + .../markupsafe-3.0.3.dist-info/METADATA | 74 + .../markupsafe-3.0.3.dist-info/RECORD | 14 + .../markupsafe-3.0.3.dist-info/WHEEL | 7 + .../licenses/LICENSE.txt | 28 + .../markupsafe-3.0.3.dist-info/top_level.txt | 1 + .../site-packages/markupsafe/__init__.py | 396 + .../site-packages/markupsafe/_native.py | 8 + .../site-packages/markupsafe/_speedups.c | 200 + .../_speedups.cpython-311-x86_64-linux-gnu.so | Bin 0 -> 43936 bytes .../site-packages/markupsafe/_speedups.pyi | 1 + .../site-packages/markupsafe/py.typed | 0 .../packaging-25.0.dist-info/INSTALLER | 1 + .../packaging-25.0.dist-info/METADATA | 105 + .../packaging-25.0.dist-info/RECORD | 40 + .../packaging-25.0.dist-info/WHEEL | 4 + .../packaging-25.0.dist-info/licenses/LICENSE | 3 + .../licenses/LICENSE.APACHE | 177 + .../licenses/LICENSE.BSD | 23 + .../site-packages/packaging/__init__.py | 15 + .../site-packages/packaging/_elffile.py | 109 + .../site-packages/packaging/_manylinux.py | 262 + .../site-packages/packaging/_musllinux.py | 85 + .../site-packages/packaging/_parser.py | 353 + .../site-packages/packaging/_structures.py | 61 + .../site-packages/packaging/_tokenizer.py | 195 + .../packaging/licenses/__init__.py | 145 + .../site-packages/packaging/licenses/_spdx.py | 759 ++ .../site-packages/packaging/markers.py | 362 + .../site-packages/packaging/metadata.py | 862 ++ .../site-packages/packaging/py.typed | 0 .../site-packages/packaging/requirements.py | 91 + .../site-packages/packaging/specifiers.py | 1019 ++ .../site-packages/packaging/tags.py | 656 ++ .../site-packages/packaging/utils.py | 163 + .../site-packages/packaging/version.py | 582 ++ .../pip-25.2.dist-info/INSTALLER | 1 + .../site-packages/pip-25.2.dist-info/METADATA | 112 + .../site-packages/pip-25.2.dist-info/RECORD | 860 ++ .../pip-25.2.dist-info/REQUESTED | 0 .../site-packages/pip-25.2.dist-info/WHEEL | 5 + .../pip-25.2.dist-info/entry_points.txt | 3 + .../pip-25.2.dist-info/licenses/AUTHORS.txt | 833 ++ .../pip-25.2.dist-info/licenses/LICENSE.txt | 20 + .../src/pip/_vendor/cachecontrol/LICENSE.txt | 13 + .../licenses/src/pip/_vendor/certifi/LICENSE | 20 + .../pip/_vendor/dependency_groups/LICENSE.txt | 9 + .../src/pip/_vendor/distlib/LICENSE.txt | 284 + .../licenses/src/pip/_vendor/distro/LICENSE | 202 + .../licenses/src/pip/_vendor/idna/LICENSE.md | 31 + .../licenses/src/pip/_vendor/msgpack/COPYING | 14 + .../src/pip/_vendor/packaging/LICENSE | 3 + .../src/pip/_vendor/packaging/LICENSE.APACHE | 177 + .../src/pip/_vendor/packaging/LICENSE.BSD | 23 + .../src/pip/_vendor/pkg_resources/LICENSE | 17 + .../src/pip/_vendor/platformdirs/LICENSE | 21 + .../licenses/src/pip/_vendor/pygments/LICENSE | 25 + .../src/pip/_vendor/pyproject_hooks/LICENSE | 21 + .../licenses/src/pip/_vendor/requests/LICENSE | 175 + .../src/pip/_vendor/resolvelib/LICENSE | 13 + .../licenses/src/pip/_vendor/rich/LICENSE | 19 + .../licenses/src/pip/_vendor/tomli/LICENSE | 21 + .../src/pip/_vendor/tomli/LICENSE-HEADER | 3 + .../licenses/src/pip/_vendor/tomli_w/LICENSE | 21 + .../src/pip/_vendor/truststore/LICENSE | 21 + .../src/pip/_vendor/urllib3/LICENSE.txt | 21 + .../pip-25.2.dist-info/top_level.txt | 1 + .../python3.11/site-packages/pip/__init__.py | 13 + .../python3.11/site-packages/pip/__main__.py | 24 + .../site-packages/pip/__pip-runner__.py | 50 + .../site-packages/pip/_internal/__init__.py | 18 + .../site-packages/pip/_internal/build_env.py | 349 + .../site-packages/pip/_internal/cache.py | 291 + .../pip/_internal/cli/__init__.py | 3 + .../pip/_internal/cli/autocompletion.py | 184 + .../pip/_internal/cli/base_command.py | 244 + .../pip/_internal/cli/cmdoptions.py | 1138 +++ .../pip/_internal/cli/command_context.py | 28 + .../pip/_internal/cli/index_command.py | 175 + .../site-packages/pip/_internal/cli/main.py | 80 + .../pip/_internal/cli/main_parser.py | 134 + .../site-packages/pip/_internal/cli/parser.py | 298 + .../pip/_internal/cli/progress_bars.py | 151 + .../pip/_internal/cli/req_command.py | 351 + .../pip/_internal/cli/spinners.py | 235 + .../pip/_internal/cli/status_codes.py | 6 + .../pip/_internal/commands/__init__.py | 139 + .../pip/_internal/commands/cache.py | 231 + .../pip/_internal/commands/check.py | 66 + .../pip/_internal/commands/completion.py | 135 + .../pip/_internal/commands/configuration.py | 288 + .../pip/_internal/commands/debug.py | 203 + .../pip/_internal/commands/download.py | 145 + .../pip/_internal/commands/freeze.py | 107 + .../pip/_internal/commands/hash.py | 58 + .../pip/_internal/commands/help.py | 40 + .../pip/_internal/commands/index.py | 159 + .../pip/_internal/commands/inspect.py | 92 + .../pip/_internal/commands/install.py | 798 ++ .../pip/_internal/commands/list.py | 400 + .../pip/_internal/commands/lock.py | 170 + .../pip/_internal/commands/search.py | 178 + .../pip/_internal/commands/show.py | 231 + .../pip/_internal/commands/uninstall.py | 113 + .../pip/_internal/commands/wheel.py | 181 + .../pip/_internal/configuration.py | 397 + .../pip/_internal/distributions/__init__.py | 21 + .../pip/_internal/distributions/base.py | 55 + .../pip/_internal/distributions/installed.py | 33 + .../pip/_internal/distributions/sdist.py | 165 + .../pip/_internal/distributions/wheel.py | 44 + .../site-packages/pip/_internal/exceptions.py | 881 ++ .../pip/_internal/index/__init__.py | 1 + .../pip/_internal/index/collector.py | 489 + .../pip/_internal/index/package_finder.py | 1059 ++ .../pip/_internal/index/sources.py | 287 + .../pip/_internal/locations/__init__.py | 441 + .../pip/_internal/locations/_distutils.py | 173 + .../pip/_internal/locations/_sysconfig.py | 215 + .../pip/_internal/locations/base.py | 82 + .../site-packages/pip/_internal/main.py | 12 + .../pip/_internal/metadata/__init__.py | 164 + .../pip/_internal/metadata/_json.py | 87 + .../pip/_internal/metadata/base.py | 685 ++ .../_internal/metadata/importlib/__init__.py | 6 + .../_internal/metadata/importlib/_compat.py | 87 + .../_internal/metadata/importlib/_dists.py | 223 + .../pip/_internal/metadata/importlib/_envs.py | 143 + .../pip/_internal/metadata/pkg_resources.py | 298 + .../pip/_internal/models/__init__.py | 1 + .../pip/_internal/models/candidate.py | 25 + .../pip/_internal/models/direct_url.py | 227 + .../pip/_internal/models/format_control.py | 78 + .../pip/_internal/models/index.py | 28 + .../_internal/models/installation_report.py | 57 + .../pip/_internal/models/link.py | 613 ++ .../pip/_internal/models/pylock.py | 188 + .../pip/_internal/models/scheme.py | 25 + .../pip/_internal/models/search_scope.py | 126 + .../pip/_internal/models/selection_prefs.py | 53 + .../pip/_internal/models/target_python.py | 122 + .../pip/_internal/models/wheel.py | 141 + .../pip/_internal/network/__init__.py | 1 + .../pip/_internal/network/auth.py | 564 ++ .../pip/_internal/network/cache.py | 133 + .../pip/_internal/network/download.py | 342 + .../pip/_internal/network/lazy_wheel.py | 213 + .../pip/_internal/network/session.py | 528 + .../pip/_internal/network/utils.py | 98 + .../pip/_internal/network/xmlrpc.py | 61 + .../pip/_internal/operations/__init__.py | 0 .../_internal/operations/build/__init__.py | 0 .../operations/build/build_tracker.py | 140 + .../_internal/operations/build/metadata.py | 38 + .../operations/build/metadata_editable.py | 41 + .../operations/build/metadata_legacy.py | 73 + .../pip/_internal/operations/build/wheel.py | 38 + .../operations/build/wheel_editable.py | 47 + .../operations/build/wheel_legacy.py | 119 + .../pip/_internal/operations/check.py | 175 + .../pip/_internal/operations/freeze.py | 259 + .../_internal/operations/install/__init__.py | 1 + .../operations/install/editable_legacy.py | 48 + .../pip/_internal/operations/install/wheel.py | 746 ++ .../pip/_internal/operations/prepare.py | 742 ++ .../site-packages/pip/_internal/pyproject.py | 182 + .../pip/_internal/req/__init__.py | 105 + .../pip/_internal/req/constructors.py | 562 ++ .../pip/_internal/req/req_dependency_group.py | 75 + .../pip/_internal/req/req_file.py | 620 ++ .../pip/_internal/req/req_install.py | 937 ++ .../pip/_internal/req/req_set.py | 81 + .../pip/_internal/req/req_uninstall.py | 639 ++ .../pip/_internal/resolution/__init__.py | 0 .../pip/_internal/resolution/base.py | 20 + .../_internal/resolution/legacy/__init__.py | 0 .../_internal/resolution/legacy/resolver.py | 598 ++ .../resolution/resolvelib/__init__.py | 0 .../_internal/resolution/resolvelib/base.py | 142 + .../resolution/resolvelib/candidates.py | 582 ++ .../resolution/resolvelib/factory.py | 814 ++ .../resolution/resolvelib/found_candidates.py | 166 + .../resolution/resolvelib/provider.py | 276 + .../resolution/resolvelib/reporter.py | 85 + .../resolution/resolvelib/requirements.py | 247 + .../resolution/resolvelib/resolver.py | 336 + .../pip/_internal/self_outdated_check.py | 254 + .../pip/_internal/utils/__init__.py | 0 .../pip/_internal/utils/_jaraco_text.py | 109 + .../site-packages/pip/_internal/utils/_log.py | 38 + .../pip/_internal/utils/appdirs.py | 52 + .../pip/_internal/utils/compat.py | 85 + .../pip/_internal/utils/compatibility_tags.py | 201 + .../pip/_internal/utils/datetime.py | 10 + .../pip/_internal/utils/deprecation.py | 126 + .../pip/_internal/utils/direct_url_helpers.py | 87 + .../pip/_internal/utils/egg_link.py | 81 + .../pip/_internal/utils/entrypoints.py | 88 + .../pip/_internal/utils/filesystem.py | 152 + .../pip/_internal/utils/filetypes.py | 24 + .../pip/_internal/utils/glibc.py | 102 + .../pip/_internal/utils/hashes.py | 150 + .../pip/_internal/utils/logging.py | 364 + .../site-packages/pip/_internal/utils/misc.py | 765 ++ .../pip/_internal/utils/packaging.py | 44 + .../pip/_internal/utils/retry.py | 45 + .../pip/_internal/utils/setuptools_build.py | 149 + .../pip/_internal/utils/subprocess.py | 248 + .../pip/_internal/utils/temp_dir.py | 294 + .../pip/_internal/utils/unpacking.py | 337 + .../site-packages/pip/_internal/utils/urls.py | 55 + .../pip/_internal/utils/virtualenv.py | 105 + .../pip/_internal/utils/wheel.py | 132 + .../pip/_internal/vcs/__init__.py | 15 + .../site-packages/pip/_internal/vcs/bazaar.py | 130 + .../site-packages/pip/_internal/vcs/git.py | 571 ++ .../pip/_internal/vcs/mercurial.py | 186 + .../pip/_internal/vcs/subversion.py | 335 + .../pip/_internal/vcs/versioncontrol.py | 693 ++ .../pip/_internal/wheel_builder.py | 334 + .../site-packages/pip/_vendor/__init__.py | 117 + .../pip/_vendor/cachecontrol/__init__.py | 29 + .../pip/_vendor/cachecontrol/_cmd.py | 70 + .../pip/_vendor/cachecontrol/adapter.py | 168 + .../pip/_vendor/cachecontrol/cache.py | 75 + .../_vendor/cachecontrol/caches/__init__.py | 8 + .../_vendor/cachecontrol/caches/file_cache.py | 145 + .../cachecontrol/caches/redis_cache.py | 48 + .../pip/_vendor/cachecontrol/controller.py | 511 + .../pip/_vendor/cachecontrol/filewrapper.py | 119 + .../pip/_vendor/cachecontrol/heuristics.py | 157 + .../pip/_vendor/cachecontrol/py.typed | 0 .../pip/_vendor/cachecontrol/serialize.py | 146 + .../pip/_vendor/cachecontrol/wrapper.py | 43 + .../pip/_vendor/certifi/__init__.py | 4 + .../pip/_vendor/certifi/__main__.py | 12 + .../pip/_vendor/certifi/cacert.pem | 4778 +++++++++ .../site-packages/pip/_vendor/certifi/core.py | 83 + .../pip/_vendor/certifi/py.typed | 0 .../pip/_vendor/dependency_groups/__init__.py | 13 + .../pip/_vendor/dependency_groups/__main__.py | 65 + .../dependency_groups/_implementation.py | 209 + .../_lint_dependency_groups.py | 59 + .../_vendor/dependency_groups/_pip_wrapper.py | 62 + .../_vendor/dependency_groups/_toml_compat.py | 9 + .../pip/_vendor/dependency_groups/py.typed | 0 .../pip/_vendor/distlib/__init__.py | 33 + .../pip/_vendor/distlib/compat.py | 1137 +++ .../pip/_vendor/distlib/resources.py | 358 + .../pip/_vendor/distlib/scripts.py | 447 + .../site-packages/pip/_vendor/distlib/t32.exe | Bin 0 -> 97792 bytes .../pip/_vendor/distlib/t64-arm.exe | Bin 0 -> 182784 bytes .../site-packages/pip/_vendor/distlib/t64.exe | Bin 0 -> 108032 bytes .../site-packages/pip/_vendor/distlib/util.py | 1984 ++++ .../site-packages/pip/_vendor/distlib/w32.exe | Bin 0 -> 91648 bytes .../pip/_vendor/distlib/w64-arm.exe | Bin 0 -> 168448 bytes .../site-packages/pip/_vendor/distlib/w64.exe | Bin 0 -> 101888 bytes .../pip/_vendor/distro/__init__.py | 54 + .../pip/_vendor/distro/__main__.py | 4 + .../pip/_vendor/distro/distro.py | 1403 +++ .../site-packages/pip/_vendor/distro/py.typed | 0 .../pip/_vendor/idna/__init__.py | 45 + .../site-packages/pip/_vendor/idna/codec.py | 122 + .../site-packages/pip/_vendor/idna/compat.py | 15 + .../site-packages/pip/_vendor/idna/core.py | 437 + .../pip/_vendor/idna/idnadata.py | 4243 ++++++++ .../pip/_vendor/idna/intranges.py | 57 + .../pip/_vendor/idna/package_data.py | 1 + .../site-packages/pip/_vendor/idna/py.typed | 0 .../pip/_vendor/idna/uts46data.py | 8681 +++++++++++++++++ .../pip/_vendor/msgpack/__init__.py | 55 + .../pip/_vendor/msgpack/exceptions.py | 48 + .../site-packages/pip/_vendor/msgpack/ext.py | 170 + .../pip/_vendor/msgpack/fallback.py | 929 ++ .../pip/_vendor/packaging/__init__.py | 15 + .../pip/_vendor/packaging/_elffile.py | 109 + .../pip/_vendor/packaging/_manylinux.py | 262 + .../pip/_vendor/packaging/_musllinux.py | 85 + .../pip/_vendor/packaging/_parser.py | 353 + .../pip/_vendor/packaging/_structures.py | 61 + .../pip/_vendor/packaging/_tokenizer.py | 195 + .../_vendor/packaging/licenses/__init__.py | 145 + .../pip/_vendor/packaging/licenses/_spdx.py | 759 ++ .../pip/_vendor/packaging/markers.py | 362 + .../pip/_vendor/packaging/metadata.py | 862 ++ .../pip/_vendor/packaging/py.typed | 0 .../pip/_vendor/packaging/requirements.py | 91 + .../pip/_vendor/packaging/specifiers.py | 1019 ++ .../pip/_vendor/packaging/tags.py | 656 ++ .../pip/_vendor/packaging/utils.py | 163 + .../pip/_vendor/packaging/version.py | 582 ++ .../pip/_vendor/pkg_resources/__init__.py | 3676 +++++++ .../pip/_vendor/platformdirs/__init__.py | 631 ++ .../pip/_vendor/platformdirs/__main__.py | 55 + .../pip/_vendor/platformdirs/android.py | 249 + .../pip/_vendor/platformdirs/api.py | 299 + .../pip/_vendor/platformdirs/macos.py | 144 + .../pip/_vendor/platformdirs/py.typed | 0 .../pip/_vendor/platformdirs/unix.py | 272 + .../pip/_vendor/platformdirs/version.py | 21 + .../pip/_vendor/platformdirs/windows.py | 272 + .../pip/_vendor/pygments/__init__.py | 82 + .../pip/_vendor/pygments/__main__.py | 17 + .../pip/_vendor/pygments/console.py | 70 + .../pip/_vendor/pygments/filter.py | 70 + .../pip/_vendor/pygments/filters/__init__.py | 940 ++ .../pip/_vendor/pygments/formatter.py | 129 + .../_vendor/pygments/formatters/__init__.py | 157 + .../_vendor/pygments/formatters/_mapping.py | 23 + .../pip/_vendor/pygments/lexer.py | 963 ++ .../pip/_vendor/pygments/lexers/__init__.py | 362 + .../pip/_vendor/pygments/lexers/_mapping.py | 602 ++ .../pip/_vendor/pygments/lexers/python.py | 1201 +++ .../pip/_vendor/pygments/modeline.py | 43 + .../pip/_vendor/pygments/plugin.py | 72 + .../pip/_vendor/pygments/regexopt.py | 91 + .../pip/_vendor/pygments/scanner.py | 104 + .../pip/_vendor/pygments/sphinxext.py | 247 + .../pip/_vendor/pygments/style.py | 203 + .../pip/_vendor/pygments/styles/__init__.py | 61 + .../pip/_vendor/pygments/styles/_mapping.py | 54 + .../pip/_vendor/pygments/token.py | 214 + .../pip/_vendor/pygments/unistring.py | 153 + .../pip/_vendor/pygments/util.py | 324 + .../pip/_vendor/pyproject_hooks/__init__.py | 31 + .../pip/_vendor/pyproject_hooks/_impl.py | 410 + .../pyproject_hooks/_in_process/__init__.py | 21 + .../_in_process/_in_process.py | 389 + .../pip/_vendor/pyproject_hooks/py.typed | 0 .../pip/_vendor/requests/__init__.py | 179 + .../pip/_vendor/requests/__version__.py | 14 + .../pip/_vendor/requests/_internal_utils.py | 50 + .../pip/_vendor/requests/adapters.py | 719 ++ .../site-packages/pip/_vendor/requests/api.py | 157 + .../pip/_vendor/requests/auth.py | 314 + .../pip/_vendor/requests/certs.py | 17 + .../pip/_vendor/requests/compat.py | 90 + .../pip/_vendor/requests/cookies.py | 561 ++ .../pip/_vendor/requests/exceptions.py | 151 + .../pip/_vendor/requests/help.py | 127 + .../pip/_vendor/requests/hooks.py | 33 + .../pip/_vendor/requests/models.py | 1039 ++ .../pip/_vendor/requests/packages.py | 25 + .../pip/_vendor/requests/sessions.py | 831 ++ .../pip/_vendor/requests/status_codes.py | 128 + .../pip/_vendor/requests/structures.py | 99 + .../pip/_vendor/requests/utils.py | 1086 +++ .../pip/_vendor/resolvelib/__init__.py | 27 + .../pip/_vendor/resolvelib/providers.py | 196 + .../pip/_vendor/resolvelib/py.typed | 0 .../pip/_vendor/resolvelib/reporters.py | 55 + .../_vendor/resolvelib/resolvers/__init__.py | 27 + .../_vendor/resolvelib/resolvers/abstract.py | 47 + .../_vendor/resolvelib/resolvers/criterion.py | 48 + .../resolvelib/resolvers/exceptions.py | 57 + .../resolvelib/resolvers/resolution.py | 622 ++ .../pip/_vendor/resolvelib/structs.py | 209 + .../pip/_vendor/rich/__init__.py | 177 + .../pip/_vendor/rich/__main__.py | 245 + .../pip/_vendor/rich/_cell_widths.py | 454 + .../pip/_vendor/rich/_emoji_codes.py | 3610 +++++++ .../pip/_vendor/rich/_emoji_replace.py | 32 + .../pip/_vendor/rich/_export_format.py | 76 + .../pip/_vendor/rich/_extension.py | 10 + .../site-packages/pip/_vendor/rich/_fileno.py | 24 + .../pip/_vendor/rich/_inspect.py | 268 + .../pip/_vendor/rich/_log_render.py | 94 + .../site-packages/pip/_vendor/rich/_loop.py | 43 + .../pip/_vendor/rich/_null_file.py | 69 + .../pip/_vendor/rich/_palettes.py | 309 + .../site-packages/pip/_vendor/rich/_pick.py | 17 + .../site-packages/pip/_vendor/rich/_ratio.py | 153 + .../pip/_vendor/rich/_spinners.py | 482 + .../site-packages/pip/_vendor/rich/_stack.py | 16 + .../site-packages/pip/_vendor/rich/_timer.py | 19 + .../pip/_vendor/rich/_win32_console.py | 661 ++ .../pip/_vendor/rich/_windows.py | 71 + .../pip/_vendor/rich/_windows_renderer.py | 56 + .../site-packages/pip/_vendor/rich/_wrap.py | 93 + .../site-packages/pip/_vendor/rich/abc.py | 33 + .../site-packages/pip/_vendor/rich/align.py | 306 + .../site-packages/pip/_vendor/rich/ansi.py | 241 + .../site-packages/pip/_vendor/rich/bar.py | 93 + .../site-packages/pip/_vendor/rich/box.py | 474 + .../site-packages/pip/_vendor/rich/cells.py | 174 + .../site-packages/pip/_vendor/rich/color.py | 621 ++ .../pip/_vendor/rich/color_triplet.py | 38 + .../site-packages/pip/_vendor/rich/columns.py | 187 + .../site-packages/pip/_vendor/rich/console.py | 2680 +++++ .../pip/_vendor/rich/constrain.py | 37 + .../pip/_vendor/rich/containers.py | 167 + .../site-packages/pip/_vendor/rich/control.py | 219 + .../pip/_vendor/rich/default_styles.py | 193 + .../pip/_vendor/rich/diagnose.py | 39 + .../site-packages/pip/_vendor/rich/emoji.py | 91 + .../site-packages/pip/_vendor/rich/errors.py | 34 + .../pip/_vendor/rich/file_proxy.py | 57 + .../pip/_vendor/rich/filesize.py | 88 + .../pip/_vendor/rich/highlighter.py | 232 + .../site-packages/pip/_vendor/rich/json.py | 139 + .../site-packages/pip/_vendor/rich/jupyter.py | 101 + .../site-packages/pip/_vendor/rich/layout.py | 442 + .../site-packages/pip/_vendor/rich/live.py | 400 + .../pip/_vendor/rich/live_render.py | 106 + .../site-packages/pip/_vendor/rich/logging.py | 297 + .../site-packages/pip/_vendor/rich/markup.py | 251 + .../site-packages/pip/_vendor/rich/measure.py | 151 + .../site-packages/pip/_vendor/rich/padding.py | 141 + .../site-packages/pip/_vendor/rich/pager.py | 34 + .../site-packages/pip/_vendor/rich/palette.py | 100 + .../site-packages/pip/_vendor/rich/panel.py | 317 + .../site-packages/pip/_vendor/rich/pretty.py | 1016 ++ .../pip/_vendor/rich/progress.py | 1715 ++++ .../pip/_vendor/rich/progress_bar.py | 223 + .../site-packages/pip/_vendor/rich/prompt.py | 400 + .../pip/_vendor/rich/protocol.py | 42 + .../site-packages/pip/_vendor/rich/py.typed | 0 .../site-packages/pip/_vendor/rich/region.py | 10 + .../site-packages/pip/_vendor/rich/repr.py | 149 + .../site-packages/pip/_vendor/rich/rule.py | 130 + .../site-packages/pip/_vendor/rich/scope.py | 86 + .../site-packages/pip/_vendor/rich/screen.py | 54 + .../site-packages/pip/_vendor/rich/segment.py | 752 ++ .../site-packages/pip/_vendor/rich/spinner.py | 132 + .../site-packages/pip/_vendor/rich/status.py | 131 + .../site-packages/pip/_vendor/rich/style.py | 796 ++ .../site-packages/pip/_vendor/rich/styled.py | 42 + .../site-packages/pip/_vendor/rich/syntax.py | 985 ++ .../site-packages/pip/_vendor/rich/table.py | 1006 ++ .../pip/_vendor/rich/terminal_theme.py | 153 + .../site-packages/pip/_vendor/rich/text.py | 1361 +++ .../site-packages/pip/_vendor/rich/theme.py | 115 + .../site-packages/pip/_vendor/rich/themes.py | 5 + .../pip/_vendor/rich/traceback.py | 899 ++ .../site-packages/pip/_vendor/rich/tree.py | 257 + .../pip/_vendor/tomli/__init__.py | 8 + .../pip/_vendor/tomli/_parser.py | 770 ++ .../site-packages/pip/_vendor/tomli/_re.py | 112 + .../site-packages/pip/_vendor/tomli/_types.py | 10 + .../site-packages/pip/_vendor/tomli/py.typed | 1 + .../pip/_vendor/tomli_w/__init__.py | 4 + .../pip/_vendor/tomli_w/_writer.py | 229 + .../pip/_vendor/tomli_w/py.typed | 1 + .../pip/_vendor/truststore/__init__.py | 36 + .../pip/_vendor/truststore/_api.py | 333 + .../pip/_vendor/truststore/_macos.py | 571 ++ .../pip/_vendor/truststore/_openssl.py | 66 + .../pip/_vendor/truststore/_ssl_constants.py | 31 + .../pip/_vendor/truststore/_windows.py | 567 ++ .../pip/_vendor/truststore/py.typed | 0 .../pip/_vendor/urllib3/__init__.py | 102 + .../pip/_vendor/urllib3/_collections.py | 355 + .../pip/_vendor/urllib3/_version.py | 2 + .../pip/_vendor/urllib3/connection.py | 572 ++ .../pip/_vendor/urllib3/connectionpool.py | 1140 +++ .../pip/_vendor/urllib3/contrib/__init__.py | 0 .../urllib3/contrib/_appengine_environ.py | 36 + .../contrib/_securetransport/__init__.py | 0 .../contrib/_securetransport/bindings.py | 519 + .../contrib/_securetransport/low_level.py | 397 + .../pip/_vendor/urllib3/contrib/appengine.py | 314 + .../pip/_vendor/urllib3/contrib/ntlmpool.py | 130 + .../pip/_vendor/urllib3/contrib/pyopenssl.py | 518 + .../urllib3/contrib/securetransport.py | 920 ++ .../pip/_vendor/urllib3/contrib/socks.py | 216 + .../pip/_vendor/urllib3/exceptions.py | 323 + .../pip/_vendor/urllib3/fields.py | 274 + .../pip/_vendor/urllib3/filepost.py | 98 + .../pip/_vendor/urllib3/packages/__init__.py | 0 .../urllib3/packages/backports/__init__.py | 0 .../urllib3/packages/backports/makefile.py | 51 + .../packages/backports/weakref_finalize.py | 155 + .../pip/_vendor/urllib3/packages/six.py | 1076 ++ .../pip/_vendor/urllib3/poolmanager.py | 540 + .../pip/_vendor/urllib3/request.py | 191 + .../pip/_vendor/urllib3/response.py | 879 ++ .../pip/_vendor/urllib3/util/__init__.py | 49 + .../pip/_vendor/urllib3/util/connection.py | 149 + .../pip/_vendor/urllib3/util/proxy.py | 57 + .../pip/_vendor/urllib3/util/queue.py | 22 + .../pip/_vendor/urllib3/util/request.py | 137 + .../pip/_vendor/urllib3/util/response.py | 107 + .../pip/_vendor/urllib3/util/retry.py | 622 ++ .../pip/_vendor/urllib3/util/ssl_.py | 504 + .../urllib3/util/ssl_match_hostname.py | 159 + .../pip/_vendor/urllib3/util/ssltransport.py | 221 + .../pip/_vendor/urllib3/util/timeout.py | 271 + .../pip/_vendor/urllib3/util/url.py | 435 + .../pip/_vendor/urllib3/util/wait.py | 152 + .../site-packages/pip/_vendor/vendor.txt | 19 + .../lib/python3.11/site-packages/pip/py.typed | 4 + .../site-packages/pkg_resources/__init__.py | 3282 +++++++ .../pkg_resources/_vendor/__init__.py | 0 .../_vendor/importlib_resources/__init__.py | 36 + .../_vendor/importlib_resources/_adapters.py | 170 + .../_vendor/importlib_resources/_common.py | 104 + .../_vendor/importlib_resources/_compat.py | 98 + .../_vendor/importlib_resources/_itertools.py | 35 + .../_vendor/importlib_resources/_legacy.py | 121 + .../_vendor/importlib_resources/abc.py | 137 + .../_vendor/importlib_resources/readers.py | 122 + .../_vendor/importlib_resources/simple.py | 116 + .../pkg_resources/_vendor/jaraco/__init__.py | 0 .../pkg_resources/_vendor/jaraco/context.py | 253 + .../pkg_resources/_vendor/jaraco/functools.py | 525 + .../_vendor/jaraco/text/__init__.py | 599 ++ .../_vendor/more_itertools/__init__.py | 6 + .../_vendor/more_itertools/more.py | 4346 +++++++++ .../_vendor/more_itertools/recipes.py | 841 ++ .../_vendor/packaging/__about__.py | 26 + .../_vendor/packaging/__init__.py | 25 + .../_vendor/packaging/_manylinux.py | 301 + .../_vendor/packaging/_musllinux.py | 136 + .../_vendor/packaging/_structures.py | 61 + .../_vendor/packaging/markers.py | 304 + .../_vendor/packaging/requirements.py | 146 + .../_vendor/packaging/specifiers.py | 802 ++ .../pkg_resources/_vendor/packaging/tags.py | 487 + .../pkg_resources/_vendor/packaging/utils.py | 136 + .../_vendor/packaging/version.py | 504 + .../_vendor/platformdirs/__init__.py | 342 + .../_vendor/platformdirs/__main__.py | 46 + .../_vendor/platformdirs/android.py | 120 + .../pkg_resources/_vendor/platformdirs/api.py | 156 + .../_vendor/platformdirs/macos.py | 64 + .../_vendor/platformdirs/unix.py | 181 + .../_vendor/platformdirs/version.py | 4 + .../_vendor/platformdirs/windows.py | 184 + .../_vendor/pyparsing/__init__.py | 331 + .../_vendor/pyparsing/actions.py | 207 + .../pkg_resources/_vendor/pyparsing/common.py | 424 + .../pkg_resources/_vendor/pyparsing/core.py | 5814 +++++++++++ .../_vendor/pyparsing/diagram/__init__.py | 642 ++ .../_vendor/pyparsing/exceptions.py | 267 + .../_vendor/pyparsing/helpers.py | 1088 +++ .../_vendor/pyparsing/results.py | 760 ++ .../_vendor/pyparsing/testing.py | 331 + .../_vendor/pyparsing/unicode.py | 352 + .../pkg_resources/_vendor/pyparsing/util.py | 235 + .../_vendor/typing_extensions.py | 2209 +++++ .../pkg_resources/_vendor/zipp.py | 329 + .../pkg_resources/extern/__init__.py | 81 + .../python_dotenv-1.0.1.dist-info/INSTALLER | 1 + .../python_dotenv-1.0.1.dist-info/LICENSE | 27 + .../python_dotenv-1.0.1.dist-info/METADATA | 692 ++ .../python_dotenv-1.0.1.dist-info/RECORD | 26 + .../python_dotenv-1.0.1.dist-info/REQUESTED | 0 .../python_dotenv-1.0.1.dist-info/WHEEL | 5 + .../entry_points.txt | 2 + .../top_level.txt | 1 + .../setuptools-66.1.1.dist-info/INSTALLER | 1 + .../setuptools-66.1.1.dist-info/LICENSE | 19 + .../setuptools-66.1.1.dist-info/METADATA | 137 + .../setuptools-66.1.1.dist-info/RECORD | 484 + .../setuptools-66.1.1.dist-info/REQUESTED | 0 .../setuptools-66.1.1.dist-info/WHEEL | 5 + .../entry_points.txt | 57 + .../setuptools-66.1.1.dist-info/top_level.txt | 4 + .../site-packages/setuptools/__init__.py | 268 + .../setuptools/_deprecation_warning.py | 7 + .../setuptools/_distutils/__init__.py | 14 + .../setuptools/_distutils/_collections.py | 194 + .../setuptools/_distutils/_functools.py | 20 + .../setuptools/_distutils/_log.py | 4 + .../setuptools/_distutils/_macos_compat.py | 12 + .../setuptools/_distutils/_msvccompiler.py | 572 ++ .../setuptools/_distutils/archive_util.py | 280 + .../setuptools/_distutils/bcppcompiler.py | 408 + .../setuptools/_distutils/ccompiler.py | 1220 +++ .../setuptools/_distutils/cmd.py | 435 + .../setuptools/_distutils/command/__init__.py | 25 + .../_distutils/command/_framework_compat.py | 55 + .../setuptools/_distutils/command/bdist.py | 157 + .../_distutils/command/bdist_dumb.py | 144 + .../_distutils/command/bdist_rpm.py | 615 ++ .../setuptools/_distutils/command/build.py | 153 + .../_distutils/command/build_clib.py | 208 + .../_distutils/command/build_ext.py | 789 ++ .../setuptools/_distutils/command/build_py.py | 407 + .../_distutils/command/build_scripts.py | 173 + .../setuptools/_distutils/command/check.py | 151 + .../setuptools/_distutils/command/clean.py | 76 + .../setuptools/_distutils/command/config.py | 377 + .../setuptools/_distutils/command/install.py | 814 ++ .../_distutils/command/install_data.py | 84 + .../_distutils/command/install_egg_info.py | 92 + .../_distutils/command/install_headers.py | 45 + .../_distutils/command/install_lib.py | 238 + .../_distutils/command/install_scripts.py | 61 + .../_distutils/command/py37compat.py | 31 + .../setuptools/_distutils/command/register.py | 321 + .../setuptools/_distutils/command/sdist.py | 531 + .../setuptools/_distutils/command/upload.py | 207 + .../setuptools/_distutils/config.py | 139 + .../setuptools/_distutils/core.py | 291 + .../setuptools/_distutils/cygwinccompiler.py | 358 + .../setuptools/_distutils/debug.py | 5 + .../setuptools/_distutils/dep_util.py | 96 + .../setuptools/_distutils/dir_util.py | 243 + .../setuptools/_distutils/dist.py | 1287 +++ .../setuptools/_distutils/errors.py | 127 + .../setuptools/_distutils/extension.py | 248 + .../setuptools/_distutils/fancy_getopt.py | 470 + .../setuptools/_distutils/file_util.py | 249 + .../setuptools/_distutils/filelist.py | 371 + .../setuptools/_distutils/log.py | 57 + .../setuptools/_distutils/msvc9compiler.py | 832 ++ .../setuptools/_distutils/msvccompiler.py | 695 ++ .../setuptools/_distutils/py38compat.py | 8 + .../setuptools/_distutils/py39compat.py | 22 + .../setuptools/_distutils/spawn.py | 109 + .../setuptools/_distutils/sysconfig.py | 552 ++ .../setuptools/_distutils/text_file.py | 287 + .../setuptools/_distutils/unixccompiler.py | 401 + .../setuptools/_distutils/util.py | 513 + .../setuptools/_distutils/version.py | 358 + .../setuptools/_distutils/versionpredicate.py | 175 + .../site-packages/setuptools/_entry_points.py | 94 + .../site-packages/setuptools/_imp.py | 82 + .../site-packages/setuptools/_importlib.py | 47 + .../site-packages/setuptools/_itertools.py | 23 + .../site-packages/setuptools/_path.py | 29 + .../site-packages/setuptools/_reqs.py | 19 + .../setuptools/_vendor/__init__.py | 0 .../_vendor/importlib_metadata/__init__.py | 1047 ++ .../_vendor/importlib_metadata/_adapters.py | 68 + .../importlib_metadata/_collections.py | 30 + .../_vendor/importlib_metadata/_compat.py | 71 + .../_vendor/importlib_metadata/_functools.py | 104 + .../_vendor/importlib_metadata/_itertools.py | 73 + .../_vendor/importlib_metadata/_meta.py | 48 + .../_vendor/importlib_metadata/_text.py | 99 + .../_vendor/importlib_resources/__init__.py | 36 + .../_vendor/importlib_resources/_adapters.py | 170 + .../_vendor/importlib_resources/_common.py | 104 + .../_vendor/importlib_resources/_compat.py | 98 + .../_vendor/importlib_resources/_itertools.py | 35 + .../_vendor/importlib_resources/_legacy.py | 121 + .../_vendor/importlib_resources/abc.py | 137 + .../_vendor/importlib_resources/readers.py | 122 + .../_vendor/importlib_resources/simple.py | 116 + .../setuptools/_vendor/jaraco/__init__.py | 0 .../setuptools/_vendor/jaraco/context.py | 253 + .../setuptools/_vendor/jaraco/functools.py | 525 + .../_vendor/jaraco/text/__init__.py | 599 ++ .../_vendor/more_itertools/__init__.py | 4 + .../setuptools/_vendor/more_itertools/more.py | 3824 ++++++++ .../_vendor/more_itertools/recipes.py | 620 ++ .../setuptools/_vendor/ordered_set.py | 488 + .../setuptools/_vendor/packaging/__about__.py | 26 + .../setuptools/_vendor/packaging/__init__.py | 25 + .../_vendor/packaging/_manylinux.py | 301 + .../_vendor/packaging/_musllinux.py | 136 + .../_vendor/packaging/_structures.py | 61 + .../setuptools/_vendor/packaging/markers.py | 304 + .../_vendor/packaging/requirements.py | 146 + .../_vendor/packaging/specifiers.py | 802 ++ .../setuptools/_vendor/packaging/tags.py | 487 + .../setuptools/_vendor/packaging/utils.py | 136 + .../setuptools/_vendor/packaging/version.py | 504 + .../setuptools/_vendor/pyparsing/__init__.py | 331 + .../setuptools/_vendor/pyparsing/actions.py | 207 + .../setuptools/_vendor/pyparsing/common.py | 424 + .../setuptools/_vendor/pyparsing/core.py | 5814 +++++++++++ .../_vendor/pyparsing/diagram/__init__.py | 642 ++ .../_vendor/pyparsing/exceptions.py | 267 + .../setuptools/_vendor/pyparsing/helpers.py | 1088 +++ .../setuptools/_vendor/pyparsing/results.py | 760 ++ .../setuptools/_vendor/pyparsing/testing.py | 331 + .../setuptools/_vendor/pyparsing/unicode.py | 352 + .../setuptools/_vendor/pyparsing/util.py | 235 + .../setuptools/_vendor/tomli/__init__.py | 11 + .../setuptools/_vendor/tomli/_parser.py | 691 ++ .../setuptools/_vendor/tomli/_re.py | 107 + .../setuptools/_vendor/tomli/_types.py | 10 + .../setuptools/_vendor/typing_extensions.py | 2296 +++++ .../site-packages/setuptools/_vendor/zipp.py | 329 + .../site-packages/setuptools/archive_util.py | 213 + .../site-packages/setuptools/build_meta.py | 512 + .../site-packages/setuptools/cli-32.exe | Bin 0 -> 65536 bytes .../site-packages/setuptools/cli-64.exe | Bin 0 -> 74752 bytes .../site-packages/setuptools/cli-arm64.exe | Bin 0 -> 137216 bytes .../site-packages/setuptools/cli.exe | Bin 0 -> 65536 bytes .../setuptools/command/__init__.py | 12 + .../site-packages/setuptools/command/alias.py | 78 + .../setuptools/command/bdist_egg.py | 457 + .../setuptools/command/bdist_rpm.py | 40 + .../site-packages/setuptools/command/build.py | 146 + .../setuptools/command/build_clib.py | 101 + .../setuptools/command/build_ext.py | 383 + .../setuptools/command/build_py.py | 368 + .../setuptools/command/develop.py | 193 + .../setuptools/command/dist_info.py | 142 + .../setuptools/command/easy_install.py | 2366 +++++ .../setuptools/command/editable_wheel.py | 844 ++ .../setuptools/command/egg_info.py | 775 ++ .../setuptools/command/install.py | 139 + .../setuptools/command/install_egg_info.py | 83 + .../setuptools/command/install_lib.py | 148 + .../setuptools/command/install_scripts.py | 70 + .../setuptools/command/launcher manifest.xml | 15 + .../setuptools/command/py36compat.py | 134 + .../setuptools/command/register.py | 18 + .../setuptools/command/rotate.py | 64 + .../setuptools/command/saveopts.py | 22 + .../site-packages/setuptools/command/sdist.py | 210 + .../setuptools/command/setopt.py | 149 + .../site-packages/setuptools/command/test.py | 251 + .../setuptools/command/upload.py | 17 + .../setuptools/command/upload_docs.py | 212 + .../setuptools/config/__init__.py | 35 + .../setuptools/config/_apply_pyprojecttoml.py | 384 + .../config/_validate_pyproject/__init__.py | 34 + .../_validate_pyproject/error_reporting.py | 318 + .../_validate_pyproject/extra_validations.py | 36 + .../fastjsonschema_exceptions.py | 51 + .../fastjsonschema_validations.py | 1035 ++ .../config/_validate_pyproject/formats.py | 259 + .../site-packages/setuptools/config/expand.py | 462 + .../setuptools/config/pyprojecttoml.py | 498 + .../setuptools/config/setupcfg.py | 769 ++ .../site-packages/setuptools/dep_util.py | 25 + .../site-packages/setuptools/depends.py | 176 + .../site-packages/setuptools/discovery.py | 601 ++ .../site-packages/setuptools/dist.py | 1218 +++ .../site-packages/setuptools/errors.py | 58 + .../site-packages/setuptools/extension.py | 148 + .../setuptools/extern/__init__.py | 76 + .../site-packages/setuptools/glob.py | 167 + .../site-packages/setuptools/gui-32.exe | Bin 0 -> 65536 bytes .../site-packages/setuptools/gui-64.exe | Bin 0 -> 75264 bytes .../site-packages/setuptools/gui-arm64.exe | Bin 0 -> 137728 bytes .../site-packages/setuptools/gui.exe | Bin 0 -> 65536 bytes .../site-packages/setuptools/installer.py | 104 + .../site-packages/setuptools/launch.py | 36 + .../site-packages/setuptools/logging.py | 37 + .../site-packages/setuptools/monkey.py | 165 + .../site-packages/setuptools/msvc.py | 1703 ++++ .../site-packages/setuptools/namespaces.py | 107 + .../site-packages/setuptools/package_index.py | 1181 +++ .../site-packages/setuptools/py34compat.py | 13 + .../site-packages/setuptools/sandbox.py | 530 + .../setuptools/script (dev).tmpl | 6 + .../site-packages/setuptools/script.tmpl | 3 + .../site-packages/setuptools/unicode_utils.py | 42 + .../site-packages/setuptools/version.py | 6 + .../site-packages/setuptools/wheel.py | 222 + .../setuptools/windows_support.py | 29 + .../werkzeug-3.1.3.dist-info/INSTALLER | 1 + .../werkzeug-3.1.3.dist-info/LICENSE.txt | 28 + .../werkzeug-3.1.3.dist-info/METADATA | 99 + .../werkzeug-3.1.3.dist-info/RECORD | 116 + .../werkzeug-3.1.3.dist-info/WHEEL | 4 + .../site-packages/werkzeug/__init__.py | 4 + .../site-packages/werkzeug/_internal.py | 211 + .../site-packages/werkzeug/_reloader.py | 471 + .../werkzeug/datastructures/__init__.py | 64 + .../werkzeug/datastructures/accept.py | 350 + .../werkzeug/datastructures/auth.py | 317 + .../werkzeug/datastructures/cache_control.py | 273 + .../werkzeug/datastructures/csp.py | 100 + .../werkzeug/datastructures/etag.py | 106 + .../werkzeug/datastructures/file_storage.py | 209 + .../werkzeug/datastructures/headers.py | 662 ++ .../werkzeug/datastructures/mixins.py | 317 + .../werkzeug/datastructures/range.py | 214 + .../werkzeug/datastructures/structures.py | 1239 +++ .../site-packages/werkzeug/debug/__init__.py | 565 ++ .../site-packages/werkzeug/debug/console.py | 219 + .../site-packages/werkzeug/debug/repr.py | 282 + .../werkzeug/debug/shared/ICON_LICENSE.md | 6 + .../werkzeug/debug/shared/console.png | Bin 0 -> 507 bytes .../werkzeug/debug/shared/debugger.js | 344 + .../werkzeug/debug/shared/less.png | Bin 0 -> 191 bytes .../werkzeug/debug/shared/more.png | Bin 0 -> 200 bytes .../werkzeug/debug/shared/style.css | 150 + .../site-packages/werkzeug/debug/tbtools.py | 450 + .../site-packages/werkzeug/exceptions.py | 894 ++ .../site-packages/werkzeug/formparser.py | 430 + .../python3.11/site-packages/werkzeug/http.py | 1405 +++ .../site-packages/werkzeug/local.py | 653 ++ .../werkzeug/middleware/__init__.py | 0 .../werkzeug/middleware/dispatcher.py | 81 + .../werkzeug/middleware/http_proxy.py | 236 + .../site-packages/werkzeug/middleware/lint.py | 439 + .../werkzeug/middleware/profiler.py | 155 + .../werkzeug/middleware/proxy_fix.py | 183 + .../werkzeug/middleware/shared_data.py | 283 + .../site-packages/werkzeug/py.typed | 0 .../werkzeug/routing/__init__.py | 134 + .../werkzeug/routing/converters.py | 261 + .../werkzeug/routing/exceptions.py | 152 + .../site-packages/werkzeug/routing/map.py | 951 ++ .../site-packages/werkzeug/routing/matcher.py | 202 + .../site-packages/werkzeug/routing/rules.py | 928 ++ .../site-packages/werkzeug/sansio/__init__.py | 0 .../site-packages/werkzeug/sansio/http.py | 170 + .../werkzeug/sansio/multipart.py | 323 + .../site-packages/werkzeug/sansio/request.py | 534 + .../site-packages/werkzeug/sansio/response.py | 763 ++ .../site-packages/werkzeug/sansio/utils.py | 167 + .../site-packages/werkzeug/security.py | 166 + .../site-packages/werkzeug/serving.py | 1125 +++ .../python3.11/site-packages/werkzeug/test.py | 1464 +++ .../site-packages/werkzeug/testapp.py | 194 + .../python3.11/site-packages/werkzeug/urls.py | 203 + .../site-packages/werkzeug/user_agent.py | 47 + .../site-packages/werkzeug/utils.py | 691 ++ .../werkzeug/wrappers/__init__.py | 3 + .../werkzeug/wrappers/request.py | 650 ++ .../werkzeug/wrappers/response.py | 831 ++ .../python3.11/site-packages/werkzeug/wsgi.py | 595 ++ netdeploy/lib64 | 1 + netdeploy/pyvenv.cfg | 5 + requirements.txt | 2 + tempclone | 1 + templates/about.html | 45 + templates/admin.html | 147 + templates/base.html | 54 + templates/contact.html | 41 + templates/index.html | 162 + templates/login.html | 42 + templates/new_request_email.html | 41 + templates/quote_email.html | 55 + templates/services.html | 67 + templates/thanks.html | 8 + wsgi.py | 6 + 1364 files changed, 364313 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 netdeploy/bin/Activate.ps1 create mode 100644 netdeploy/bin/activate create mode 100644 netdeploy/bin/activate.csh create mode 100644 netdeploy/bin/activate.fish create mode 100755 netdeploy/bin/dotenv create mode 100755 netdeploy/bin/flask create mode 100755 netdeploy/bin/gunicorn create mode 100755 netdeploy/bin/pip create mode 100755 netdeploy/bin/pip3 create mode 100755 netdeploy/bin/pip3.11 create mode 120000 netdeploy/bin/python create mode 120000 netdeploy/bin/python3 create mode 120000 netdeploy/bin/python3.11 create mode 100644 netdeploy/include/site/python3.11/greenlet/greenlet.h create mode 100644 netdeploy/lib/python3.11/site-packages/_distutils_hack/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/_distutils_hack/override.py create mode 100644 netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/blinker/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/blinker/_utilities.py create mode 100644 netdeploy/lib/python3.11/site-packages/blinker/base.py create mode 100644 netdeploy/lib/python3.11/site-packages/blinker/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/licenses/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/click/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/_compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/_termui_impl.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/_textwrap.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/_utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/_winconsole.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/core.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/decorators.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/formatting.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/globals.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/parser.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/click/shell_completion.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/termui.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/testing.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/types.py create mode 100644 netdeploy/lib/python3.11/site-packages/click/utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/distutils-precedence.pth create mode 100644 netdeploy/lib/python3.11/site-packages/dns/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/_asyncbackend.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/_asyncio_backend.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/_ddr.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/_features.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/_immutable_ctx.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/_no_ssl.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/_tls_util.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/_trio_backend.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/asyncbackend.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/asyncquery.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/asyncresolver.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/btree.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/btreezone.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/dnssec.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/base.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/cryptography.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/dsa.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/ecdsa.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/eddsa.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/rsa.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/dnssectypes.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/e164.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/edns.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/entropy.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/enum.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/exception.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/flags.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/grange.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/immutable.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/inet.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/ipv4.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/ipv6.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/message.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/name.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/namedict.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/nameserver.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/node.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/opcode.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/dns/query.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/quic/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/quic/_asyncio.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/quic/_common.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/quic/_sync.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/quic/_trio.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rcode.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdata.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdataclass.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdataset.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdatatype.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AFSDB.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AMTRELAY.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AVC.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CAA.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CDNSKEY.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CDS.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CERT.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CNAME.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CSYNC.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DLV.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DNAME.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DNSKEY.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DS.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DSYNC.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI48.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI64.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/GPOS.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/HINFO.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/HIP.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/ISDN.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/L32.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/L64.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/LOC.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/LP.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/MX.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NID.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NINFO.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NS.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/OPT.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/PTR.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RESINFO.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RP.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RRSIG.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RT.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SMIMEA.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SOA.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SPF.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SSHFP.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TKEY.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TLSA.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TSIG.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TXT.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/URI.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/WALLET.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/X25.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/ZONEMD.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/CH/A.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/CH/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/A.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/AAAA.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/APL.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/DHCID.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/HTTPS.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/IPSECKEY.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/KX.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NAPTR.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP_PTR.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/PX.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/SRV.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/SVCB.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/WKS.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/dnskeybase.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/dsbase.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/euibase.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/mxbase.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/nsbase.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/svcbbase.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/tlsabase.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/txtbase.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rdtypes/util.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/renderer.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/resolver.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/reversename.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/rrset.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/serial.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/set.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/tokenizer.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/transaction.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/tsig.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/tsigkeyring.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/ttl.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/update.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/versioned.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/win32util.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/wire.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/xfr.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/zone.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/zonefile.py create mode 100644 netdeploy/lib/python3.11/site-packages/dns/zonetypes.py create mode 100644 netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/licenses/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/dotenv/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/dotenv/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/dotenv/cli.py create mode 100644 netdeploy/lib/python3.11/site-packages/dotenv/ipython.py create mode 100644 netdeploy/lib/python3.11/site-packages/dotenv/main.py create mode 100644 netdeploy/lib/python3.11/site-packages/dotenv/parser.py create mode 100644 netdeploy/lib/python3.11/site-packages/dotenv/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/dotenv/variables.py create mode 100644 netdeploy/lib/python3.11/site-packages/dotenv/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/REQUESTED create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/AUTHORS create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/_version.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/asyncio.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/backdoor.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/convenience.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/corolocal.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/coros.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/dagpool.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/db_pool.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/debug.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/event.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/BaseHTTPServer.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/CGIHTTPServer.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/MySQLdb.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/SSL.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/crypto.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/tsafe.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/Queue.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/SimpleHTTPServer.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/SocketServer.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/_socket_nodns.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/asynchat.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/asyncore.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/builtin.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/ftplib.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/http/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/http/client.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/http/cookiejar.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/http/cookies.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/http/server.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/httplib.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/os.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/profile.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/select.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/selectors.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/socket.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/ssl.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/subprocess.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/thread.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/threading.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/time.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/error.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/parse.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/request.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/response.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/urllib2.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/green/zmq.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/greenio/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/greenio/base.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/greenio/py3.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/greenpool.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/greenthread.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/hubs/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/hubs/asyncio.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/hubs/epolls.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/hubs/hub.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/hubs/kqueue.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/hubs/poll.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/hubs/pyevent.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/hubs/selects.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/hubs/timer.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/lock.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/patcher.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/pools.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/queue.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/semaphore.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/support/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/support/greendns.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/support/greenlets.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/support/psycopg2_patcher.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/support/pylib.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/support/stacklesspypys.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/support/stacklesss.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/timeout.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/tpool.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/websocket.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/wsgi.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/README.rst create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/README.rst create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore.thrift create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/constants.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/ttypes.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/api.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/client.py create mode 100755 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/example/ex1.png create mode 100755 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/example/ex2.png create mode 100755 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/example/ex3.png create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/greenthread.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/http.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/log.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/patcher.py create mode 100644 netdeploy/lib/python3.11/site-packages/eventlet/zipkin/wsgi.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/REQUESTED create mode 100644 netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/entry_points.txt create mode 100644 netdeploy/lib/python3.11/site-packages/flask/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/app.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/blueprints.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/cli.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/config.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/ctx.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/debughelpers.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/globals.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/helpers.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/json/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/json/provider.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/json/tag.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/logging.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/flask/sansio/README.md create mode 100644 netdeploy/lib/python3.11/site-packages/flask/sansio/app.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/sansio/blueprints.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/sansio/scaffold.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/sessions.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/signals.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/templating.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/testing.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/typing.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/views.py create mode 100644 netdeploy/lib/python3.11/site-packages/flask/wrappers.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE.PSF create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/top_level.txt create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/CObjects.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/PyGreenlet.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/PyGreenlet.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/PyGreenletUnswitchable.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/PyModule.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TBrokenGreenlet.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TExceptionState.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TGreenlet.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TGreenlet.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TGreenletGlobals.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TMainGreenlet.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TPythonState.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TStackState.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TThreadState.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TThreadStateCreator.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TThreadStateDestroy.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/TUserGreenlet.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/__init__.py create mode 100755 netdeploy/lib/python3.11/site-packages/greenlet/_greenlet.cpython-311-x86_64-linux-gnu.so create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet.cpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet_allocator.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet_compiler_compat.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet_cpython_compat.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet_exceptions.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet_internal.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet_msvc_compat.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet_refs.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet_slp_switch.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/greenlet_thread_support.hpp create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/setup_switch_x64_masm.cmd create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_aarch64_gcc.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_alpha_unix.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_amd64_unix.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm32_gcc.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm32_ios.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.asm create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.obj create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_msvc.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_csky_gcc.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_loongarch64_linux.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_m68k_gcc.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_mips_unix.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_aix.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_linux.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_aix.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_linux.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_macosx.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_unix.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_riscv_unix.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_s390_unix.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_sh_gcc.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_sparc_sun_gcc.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x32_unix.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.asm create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.obj create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_msvc.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x86_msvc.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x86_unix.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/slp_platformselect.h create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension.c create mode 100755 netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension.cpython-311-x86_64-linux-gnu.so create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpp create mode 100755 netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpython-311-x86_64-linux-gnu.so create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_clearing_run_switches.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_cpp_exception.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_initialstub_already_started.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_slp_switch.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets2.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_two_greenlets.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/leakcheck.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_contextvars.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_cpp.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_extension_interface.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_gc.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_generator.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_generator_nested.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_greenlet.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_greenlet_trash.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_leaks.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_stack_saved.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_throw.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_tracing.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_version.py create mode 100644 netdeploy/lib/python3.11/site-packages/greenlet/tests/test_weakref.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/REQUESTED create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/entry_points.txt create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/top_level.txt create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/app/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/app/base.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/app/pasterapp.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/app/wsgiapp.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/arbiter.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/config.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/debug.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/errors.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/glogging.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/http/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/http/body.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/http/errors.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/http/message.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/http/parser.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/http/unreader.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/http/wsgi.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/instrument/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/instrument/statsd.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/pidfile.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/reloader.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/sock.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/systemd.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/util.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/workers/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/workers/base.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/workers/base_async.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/workers/geventlet.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/workers/ggevent.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/workers/gthread.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/workers/gtornado.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/workers/sync.py create mode 100644 netdeploy/lib/python3.11/site-packages/gunicorn/workers/workertmp.py create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous/_json.py create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous/encoding.py create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous/exc.py create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous/serializer.py create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous/signer.py create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous/timed.py create mode 100644 netdeploy/lib/python3.11/site-packages/itsdangerous/url_safe.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/entry_points.txt create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/_identifier.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/async_utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/bccache.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/compiler.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/constants.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/debug.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/defaults.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/environment.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/ext.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/filters.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/idtracking.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/lexer.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/loaders.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/meta.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/nativetypes.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/nodes.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/optimizer.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/parser.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/runtime.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/sandbox.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/tests.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/jinja2/visitor.py create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/licenses/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/top_level.txt create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe/_native.py create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe/_speedups.c create mode 100755 netdeploy/lib/python3.11/site-packages/markupsafe/_speedups.cpython-311-x86_64-linux-gnu.so create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe/_speedups.pyi create mode 100644 netdeploy/lib/python3.11/site-packages/markupsafe/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/packaging-25.0.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/packaging-25.0.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/packaging-25.0.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/packaging-25.0.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/packaging-25.0.dist-info/licenses/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/packaging-25.0.dist-info/licenses/LICENSE.APACHE create mode 100644 netdeploy/lib/python3.11/site-packages/packaging-25.0.dist-info/licenses/LICENSE.BSD create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/_elffile.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/_manylinux.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/_musllinux.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/_parser.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/_structures.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/_tokenizer.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/licenses/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/licenses/_spdx.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/markers.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/metadata.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/requirements.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/specifiers.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/tags.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/packaging/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/REQUESTED create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/entry_points.txt create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/AUTHORS.txt create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/cachecontrol/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/certifi/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/dependency_groups/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/distlib/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/distro/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/idna/LICENSE.md create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/msgpack/COPYING create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE.APACHE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/packaging/LICENSE.BSD create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/pkg_resources/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/platformdirs/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/pygments/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/pyproject_hooks/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/requests/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/resolvelib/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/rich/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/tomli/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/tomli/LICENSE-HEADER create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/tomli_w/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/truststore/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/licenses/src/pip/_vendor/urllib3/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/pip-25.2.dist-info/top_level.txt create mode 100644 netdeploy/lib/python3.11/site-packages/pip/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/__pip-runner__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/build_env.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cache.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/autocompletion.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/base_command.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/cmdoptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/command_context.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/index_command.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/main.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/main_parser.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/parser.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/progress_bars.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/req_command.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/spinners.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/cli/status_codes.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/cache.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/check.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/completion.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/configuration.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/debug.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/download.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/freeze.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/hash.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/help.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/index.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/inspect.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/install.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/list.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/lock.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/search.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/show.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/uninstall.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/commands/wheel.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/configuration.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/distributions/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/distributions/base.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/distributions/installed.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/distributions/sdist.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/distributions/wheel.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/index/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/index/collector.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/index/package_finder.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/index/sources.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/locations/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/locations/_distutils.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/locations/_sysconfig.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/locations/base.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/main.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/metadata/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/metadata/_json.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/metadata/base.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/metadata/importlib/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_dists.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_envs.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/metadata/pkg_resources.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/candidate.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/direct_url.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/format_control.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/index.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/installation_report.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/link.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/pylock.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/scheme.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/search_scope.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/selection_prefs.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/target_python.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/models/wheel.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/network/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/network/auth.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/network/cache.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/network/download.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/network/lazy_wheel.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/network/session.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/network/utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/network/xmlrpc.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/build/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/build/build_tracker.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/build/metadata.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/build/metadata_editable.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/build/metadata_legacy.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/build/wheel.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/build/wheel_editable.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/build/wheel_legacy.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/check.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/freeze.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/install/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/install/editable_legacy.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/install/wheel.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/operations/prepare.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/pyproject.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/req/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/req/constructors.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/req/req_dependency_group.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/req/req_file.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/req/req_install.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/req/req_set.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/req/req_uninstall.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/base.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/legacy/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/legacy/resolver.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/base.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/candidates.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/factory.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/provider.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/reporter.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/requirements.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/resolver.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/self_outdated_check.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/_jaraco_text.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/_log.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/appdirs.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/compatibility_tags.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/datetime.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/deprecation.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/direct_url_helpers.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/egg_link.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/entrypoints.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/filesystem.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/filetypes.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/glibc.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/hashes.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/logging.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/misc.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/packaging.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/retry.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/setuptools_build.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/subprocess.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/temp_dir.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/unpacking.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/urls.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/virtualenv.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/utils/wheel.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/vcs/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/vcs/bazaar.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/vcs/git.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/vcs/mercurial.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/vcs/subversion.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/vcs/versioncontrol.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_internal/wheel_builder.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/_cmd.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/adapter.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/cache.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/caches/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/controller.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/filewrapper.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/heuristics.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/serialize.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/cachecontrol/wrapper.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/certifi/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/certifi/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/certifi/cacert.pem create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/certifi/core.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/certifi/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/dependency_groups/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/dependency_groups/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/dependency_groups/_implementation.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/dependency_groups/_lint_dependency_groups.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/dependency_groups/_pip_wrapper.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/dependency_groups/_toml_compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/dependency_groups/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/resources.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/scripts.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/t32.exe create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/t64-arm.exe create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/t64.exe create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/util.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/w32.exe create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/w64-arm.exe create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distlib/w64.exe create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distro/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distro/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distro/distro.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/distro/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/idna/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/idna/codec.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/idna/compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/idna/core.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/idna/idnadata.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/idna/intranges.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/idna/package_data.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/idna/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/idna/uts46data.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/msgpack/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/msgpack/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/msgpack/ext.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/msgpack/fallback.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/_elffile.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/_manylinux.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/_musllinux.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/_parser.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/_structures.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/_tokenizer.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/licenses/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/licenses/_spdx.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/markers.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/metadata.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/requirements.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/specifiers.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/tags.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/packaging/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pkg_resources/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/platformdirs/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/platformdirs/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/platformdirs/android.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/platformdirs/api.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/platformdirs/macos.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/platformdirs/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/platformdirs/unix.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/platformdirs/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/platformdirs/windows.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/console.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/filter.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/filters/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/formatter.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/_mapping.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/lexer.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/lexers/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/lexers/_mapping.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/lexers/python.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/modeline.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/plugin.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/regexopt.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/scanner.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/sphinxext.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/style.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/styles/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/styles/_mapping.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/token.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/unistring.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pygments/util.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_impl.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/__version__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/_internal_utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/adapters.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/api.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/auth.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/certs.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/cookies.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/help.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/hooks.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/models.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/packages.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/sessions.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/status_codes.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/structures.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/requests/utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/resolvelib/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/resolvelib/providers.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/resolvelib/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/resolvelib/reporters.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers/abstract.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers/criterion.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers/resolution.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/resolvelib/structs.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_cell_widths.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_emoji_codes.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_emoji_replace.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_export_format.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_extension.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_fileno.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_inspect.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_log_render.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_loop.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_null_file.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_palettes.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_pick.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_ratio.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_spinners.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_stack.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_timer.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_win32_console.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_windows.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_windows_renderer.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/_wrap.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/abc.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/align.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/ansi.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/bar.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/box.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/cells.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/color.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/color_triplet.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/columns.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/console.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/constrain.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/containers.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/control.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/default_styles.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/diagnose.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/emoji.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/errors.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/file_proxy.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/filesize.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/highlighter.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/json.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/jupyter.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/layout.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/live.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/live_render.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/logging.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/markup.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/measure.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/padding.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/pager.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/palette.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/panel.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/pretty.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/progress.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/progress_bar.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/prompt.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/protocol.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/region.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/repr.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/rule.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/scope.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/screen.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/segment.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/spinner.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/status.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/style.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/styled.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/syntax.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/table.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/terminal_theme.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/text.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/theme.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/themes.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/traceback.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/rich/tree.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/tomli/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/tomli/_parser.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/tomli/_re.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/tomli/_types.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/tomli/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/tomli_w/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/tomli_w/_writer.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/tomli_w/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/truststore/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/truststore/_api.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/truststore/_macos.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/truststore/_openssl.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/truststore/_ssl_constants.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/truststore/_windows.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/truststore/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/_collections.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/_version.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/connection.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/connectionpool.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/_appengine_environ.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/_securetransport/bindings.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/_securetransport/low_level.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/appengine.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/ntlmpool.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/pyopenssl.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/securetransport.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/socks.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/fields.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/filepost.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/backports/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/backports/makefile.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/backports/weakref_finalize.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/six.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/poolmanager.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/request.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/response.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/connection.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/proxy.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/queue.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/request.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/response.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/retry.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/ssl_.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/ssl_match_hostname.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/ssltransport.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/timeout.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/url.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/urllib3/util/wait.py create mode 100644 netdeploy/lib/python3.11/site-packages/pip/_vendor/vendor.txt create mode 100644 netdeploy/lib/python3.11/site-packages/pip/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/_adapters.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/_common.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/_compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/_itertools.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/_legacy.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/abc.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/readers.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/simple.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/jaraco/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/jaraco/context.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/jaraco/functools.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/jaraco/text/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/more_itertools/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/more_itertools/more.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/more_itertools/recipes.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/__about__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/_manylinux.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/_musllinux.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/_structures.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/markers.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/requirements.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/specifiers.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/tags.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/__main__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/android.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/api.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/macos.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/unix.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/windows.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/actions.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/common.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/core.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/diagram/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/helpers.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/results.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/testing.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/unicode.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/util.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/typing_extensions.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/_vendor/zipp.py create mode 100644 netdeploy/lib/python3.11/site-packages/pkg_resources/extern/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/python_dotenv-1.0.1.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/python_dotenv-1.0.1.dist-info/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/python_dotenv-1.0.1.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/python_dotenv-1.0.1.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/python_dotenv-1.0.1.dist-info/REQUESTED create mode 100644 netdeploy/lib/python3.11/site-packages/python_dotenv-1.0.1.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/python_dotenv-1.0.1.dist-info/entry_points.txt create mode 100644 netdeploy/lib/python3.11/site-packages/python_dotenv-1.0.1.dist-info/top_level.txt create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/LICENSE create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/REQUESTED create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/entry_points.txt create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/top_level.txt create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_deprecation_warning.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/_collections.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/_functools.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/_log.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/_macos_compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/_msvccompiler.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/archive_util.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/bcppcompiler.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/ccompiler.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/cmd.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/_framework_compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/bdist.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/bdist_dumb.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/bdist_rpm.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/build.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/build_clib.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/build_ext.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/build_py.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/build_scripts.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/check.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/clean.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/config.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/install.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/install_data.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/install_egg_info.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/install_headers.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/install_lib.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/install_scripts.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/py37compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/register.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/sdist.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/command/upload.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/config.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/core.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/cygwinccompiler.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/debug.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/dep_util.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/dir_util.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/dist.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/errors.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/extension.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/fancy_getopt.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/file_util.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/filelist.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/log.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/msvc9compiler.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/msvccompiler.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/py38compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/py39compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/spawn.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/sysconfig.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/text_file.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/unixccompiler.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/util.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_distutils/versionpredicate.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_entry_points.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_imp.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_importlib.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_itertools.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_path.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_reqs.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_adapters.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_collections.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_functools.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_itertools.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_meta.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_text.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/_adapters.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/_common.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/_compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/_itertools.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/_legacy.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/abc.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/readers.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/simple.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/jaraco/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/jaraco/context.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/jaraco/functools.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/jaraco/text/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/more_itertools/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/more_itertools/more.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/more_itertools/recipes.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/ordered_set.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/__about__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/_manylinux.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/_musllinux.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/_structures.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/markers.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/requirements.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/specifiers.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/tags.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/packaging/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/actions.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/common.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/core.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/diagram/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/helpers.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/results.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/testing.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/unicode.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/util.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/tomli/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/tomli/_parser.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/tomli/_re.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/tomli/_types.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/typing_extensions.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/_vendor/zipp.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/archive_util.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/build_meta.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/cli-32.exe create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/cli-64.exe create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/cli-arm64.exe create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/cli.exe create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/alias.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/bdist_egg.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/bdist_rpm.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/build.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/build_clib.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/build_ext.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/build_py.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/develop.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/dist_info.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/easy_install.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/editable_wheel.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/egg_info.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/install.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/install_egg_info.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/install_lib.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/install_scripts.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/launcher manifest.xml create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/py36compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/register.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/rotate.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/saveopts.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/sdist.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/setopt.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/test.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/upload.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/command/upload_docs.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/_apply_pyprojecttoml.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/error_reporting.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/extra_validations.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/fastjsonschema_validations.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/formats.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/expand.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/pyprojecttoml.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/config/setupcfg.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/dep_util.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/depends.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/discovery.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/dist.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/errors.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/extension.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/extern/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/glob.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/gui-32.exe create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/gui-64.exe create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/gui-arm64.exe create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/gui.exe create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/installer.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/launch.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/logging.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/monkey.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/msvc.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/namespaces.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/package_index.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/py34compat.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/sandbox.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/script (dev).tmpl create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/script.tmpl create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/unicode_utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/version.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/wheel.py create mode 100644 netdeploy/lib/python3.11/site-packages/setuptools/windows_support.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug-3.1.3.dist-info/INSTALLER create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug-3.1.3.dist-info/LICENSE.txt create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug-3.1.3.dist-info/METADATA create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug-3.1.3.dist-info/RECORD create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug-3.1.3.dist-info/WHEEL create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/_internal.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/_reloader.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/accept.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/auth.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/cache_control.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/csp.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/etag.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/file_storage.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/headers.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/mixins.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/range.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/datastructures/structures.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/debug/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/debug/console.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/debug/repr.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/debug/shared/ICON_LICENSE.md create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/debug/shared/console.png create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/debug/shared/debugger.js create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/debug/shared/less.png create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/debug/shared/more.png create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/debug/shared/style.css create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/debug/tbtools.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/formparser.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/http.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/local.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/middleware/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/middleware/dispatcher.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/middleware/http_proxy.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/middleware/lint.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/middleware/profiler.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/middleware/proxy_fix.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/middleware/shared_data.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/py.typed create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/routing/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/routing/converters.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/routing/exceptions.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/routing/map.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/routing/matcher.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/routing/rules.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/sansio/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/sansio/http.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/sansio/multipart.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/sansio/request.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/sansio/response.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/sansio/utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/security.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/serving.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/test.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/testapp.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/urls.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/user_agent.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/utils.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/wrappers/__init__.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/wrappers/request.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/wrappers/response.py create mode 100644 netdeploy/lib/python3.11/site-packages/werkzeug/wsgi.py create mode 120000 netdeploy/lib64 create mode 100644 netdeploy/pyvenv.cfg create mode 100644 requirements.txt create mode 160000 tempclone create mode 100644 templates/about.html create mode 100644 templates/admin.html create mode 100644 templates/base.html create mode 100644 templates/contact.html create mode 100644 templates/index.html create mode 100644 templates/login.html create mode 100644 templates/new_request_email.html create mode 100644 templates/quote_email.html create mode 100644 templates/services.html create mode 100644 templates/thanks.html create mode 100644 wsgi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2f83d60 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Python +__pycache__/ +*.pyc +*.pyo + +# Flask env files +.env +*.env + +# Virtual environments +venv/ +.venv/ + +# Cache/storage +instance/ +*.db +*.sqlite3 + +# Logs +*.log +logs/ + +# OS files +.DS_Store diff --git a/app.py b/app.py new file mode 100644 index 0000000..008ec72 --- /dev/null +++ b/app.py @@ -0,0 +1,397 @@ +# app.py +from __future__ import annotations + +import os, smtplib, sqlite3, json, secrets, time +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText +from datetime import datetime, timedelta +from typing import Dict, Any, Tuple + +from dotenv import load_dotenv +from flask import ( + Flask, render_template, request, redirect, url_for, flash, session +) +from werkzeug.security import check_password_hash + +# ----------------------------------------------------------------------------- +# Load .env FIRST +# ----------------------------------------------------------------------------- +load_dotenv() # must come before getenv() usage + +# ----------------------------------------------------------------------------- +# Config +# ----------------------------------------------------------------------------- +def getenv(name: str, default: str | None=None) -> str | None: + return os.environ.get(name, default) + +APP_SECRET_KEY = getenv("APP_SECRET_KEY", "dev_change_me") + +# Single admin (from .env) +ADMIN_USERNAME = getenv("ADMIN_USERNAME", "") +ADMIN_PASSWORD_HASH = getenv("ADMIN_PASSWORD_HASH", "") + +ADMIN_EMAIL = getenv("ADMIN_EMAIL", "admin@example.com") +SMTP_HOST = getenv("SMTP_HOST", "") +SMTP_PORT = int(getenv("SMTP_PORT", "587") or "587") +SMTP_USER = getenv("SMTP_USER", "") +SMTP_PASS = getenv("SMTP_PASS", "") +SMTP_FROM = getenv("SMTP_FROM", SMTP_USER or "no-reply@example.com") +BASE_URL = getenv("BASE_URL", "http://localhost:5000") + +DB_PATH = getenv("DB_PATH", "quotes.db") + +# Session hardening (use HTTPS in prod) +SESSION_COOKIE_SECURE = getenv("SESSION_COOKIE_SECURE", "false").lower() == "true" +SESSION_COOKIE_HTTPONLY = getenv("SESSION_COOKIE_HTTPONLY", "true").lower() == "true" +SESSION_COOKIE_SAMESITE = getenv("SESSION_COOKIE_SAMESITE", "Lax") + +app = Flask(__name__, static_folder="static", static_url_path="/static") +app.secret_key = APP_SECRET_KEY +app.config.update( + SESSION_COOKIE_SECURE=SESSION_COOKIE_SECURE, + SESSION_COOKIE_HTTPONLY=SESSION_COOKIE_HTTPONLY, + SESSION_COOKIE_SAMESITE=SESSION_COOKIE_SAMESITE, + PERMANENT_SESSION_LIFETIME=timedelta(days=30), +) + +# ----------------------------------------------------------------------------- +# DB +# ----------------------------------------------------------------------------- +def init_db() -> None: + con = sqlite3.connect(DB_PATH) + cur = con.cursor() + cur.execute(""" + CREATE TABLE IF NOT EXISTS quote_requests ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + created_at TEXT NOT NULL, + name TEXT NOT NULL, + email TEXT NOT NULL, + phone TEXT, + company TEXT, + project_type TEXT, + complexity TEXT, + urgency TEXT, + features TEXT, + budget_range TEXT, + description TEXT, + attachments TEXT, + est_hours REAL, + est_cost REAL, + hourly_rate REAL, + json_payload TEXT + ) + """) + con.commit() + con.close() + +def migrate_db() -> None: + """Add QoL columns if missing.""" + con = sqlite3.connect(DB_PATH) + cur = con.cursor() + cur.execute("PRAGMA table_info(quote_requests)") + cols = {row[1] for row in cur.fetchall()} + + if "status" not in cols: + cur.execute("ALTER TABLE quote_requests ADD COLUMN status TEXT DEFAULT 'open'") + if "completed_at" not in cols: + cur.execute("ALTER TABLE quote_requests ADD COLUMN completed_at TEXT") + if "deleted_at" not in cols: + cur.execute("ALTER TABLE quote_requests ADD COLUMN deleted_at TEXT") + + con.commit() + con.close() + +init_db() +migrate_db() + +# ----------------------------------------------------------------------------- +# Auth helpers (sessions + CSRF + simple throttle) +# ----------------------------------------------------------------------------- +_failed: dict[str, tuple[int, float]] = {} # key -> (count, last_ts) + +def throttle_key() -> str: + return request.headers.get("X-Forwarded-For", request.remote_addr or "unknown") + +def throttled(key: str, limit=5, window=900) -> bool: + now = time.time() + count, last = _failed.get(key, (0, 0)) + if now - last > window: + count = 0 + return count >= limit + +def bump_fail(key: str) -> None: + count, _ = _failed.get(key, (0, 0)) + _failed[key] = (count + 1, time.time()) + +def csrf_token() -> str: + tok = session.get("_csrf") + if not tok: + tok = secrets.token_urlsafe(32) + session["_csrf"] = tok + return tok + +def check_csrf() -> bool: + return request.form.get("_csrf") == session.get("_csrf") + +def admin_required(view): + from functools import wraps + @wraps(view) + def _wrap(*args, **kwargs): + if session.get("is_admin"): + return view(*args, **kwargs) + return redirect(url_for("admin_login_form", next=request.path)) + return _wrap + +# ----------------------------------------------------------------------------- +# Estimator logic (plain-language form) +# ----------------------------------------------------------------------------- +def estimate_hours_and_cost(payload: Dict[str, Any]) -> Tuple[float, float, float]: + hourly_rate = float(getenv("HOURLY_RATE", "95")) + + need = payload.get("need", "not-sure") + base = { + "simple-site": 10, + "pro-site": 18, + "online-form": 8, + "sell-online": 24, + "fix-or-improve": 6, + "it-help": 6, + "custom-app": 28, + "not-sure": 8, + }.get(need, 8) + + size = payload.get("scope_size", "small") + size_mult = {"small": 1.0, "medium": 1.4, "large": 2.0}.get(size, 1.0) + + timeline = payload.get("timeline", "flexible") + time_mult = {"flexible": 1.0, "soon": 1.1, "rush": 1.25, "critical": 1.45}.get(timeline, 1.0) + + hours = base * size_mult * time_mult + + extras = payload.get("extras", []) + if isinstance(extras, str): + extras = [extras] + extra_add = {"content": 3, "branding": 4, "training": 2, "care": 2} + for e in extras: + hours += extra_add.get(e, 0) + + hours = max(3, round(hours, 1)) + cost = round(hours * hourly_rate, 2) + return hours, cost, hourly_rate + +# ----------------------------------------------------------------------------- +# Email +# ----------------------------------------------------------------------------- +def send_email(subject: str, html_body: str, to_address: str) -> bool: + if not SMTP_HOST or not to_address: + app.logger.info("SMTP not configured; would send to %s (%s)", to_address, subject) + return False + try: + msg = MIMEMultipart("alternative") + msg["Subject"] = subject + msg["From"] = SMTP_FROM + msg["To"] = to_address + msg.attach(MIMEText(html_body, "html")) + with smtplib.SMTP(SMTP_HOST, SMTP_PORT) as server: + server.starttls() + if SMTP_USER and SMTP_PASS: + server.login(SMTP_USER, SMTP_PASS) + server.sendmail(SMTP_FROM, [to_address], msg.as_string()) + return True + except Exception as e: + app.logger.error("Email send failed: %s", e) + return False + +# ----------------------------------------------------------------------------- +# Routes: public +# ----------------------------------------------------------------------------- +@app.get("/") +def index(): + return render_template("index.html") + +@app.post("/submit") +def submit(): + payload = { + "name": request.form.get("name","").strip(), + "email": request.form.get("email","").strip(), + "phone": request.form.get("phone","").strip(), + "company": request.form.get("company","").strip(), + + "need": request.form.get("need","not-sure"), + "scope_size": request.form.get("scope_size","small"), + "timeline": request.form.get("timeline","flexible"), + "extras": request.form.getlist("extras"), + "budget_feel": request.form.get("budget_feel","unsure"), + + "description": request.form.get("description","").strip(), + } + + if not payload["name"] or not payload["email"]: + flash("Name and Email are required.", "error") + return redirect(url_for("index")) + + est_hours, est_cost, hourly_rate = estimate_hours_and_cost(payload) + + con = sqlite3.connect(DB_PATH) + cur = con.cursor() + cur.execute(""" + INSERT INTO quote_requests + (created_at, name, email, phone, company, + project_type, complexity, urgency, features, budget_range, + description, attachments, est_hours, est_cost, hourly_rate, json_payload) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, ( + datetime.utcnow().isoformat(), + payload["name"], payload["email"], payload["phone"], payload["company"], + payload["need"], payload["scope_size"], payload["timeline"], + ",".join(payload["extras"]), payload["budget_feel"], + payload["description"], "", + est_hours, est_cost, hourly_rate, json.dumps(payload) + )) + con.commit() + con.close() + + admin_html = render_template("new_request_email.html", + payload=payload, + est_hours=est_hours, + est_cost=est_cost, + hourly_rate=hourly_rate, + base_url=BASE_URL) + send_email("New Quote Request Received", admin_html, ADMIN_EMAIL) + return redirect(url_for("thanks")) + +@app.get("/thanks") +def thanks(): + return render_template("thanks.html") + +# ----------------------------------------------------------------------------- +# Routes: auth +# ----------------------------------------------------------------------------- +@app.get("/admin/login") +def admin_login_form(): + return render_template("login.html", csrf=csrf_token(), next=request.args.get("next","/admin")) + +@app.post("/admin/login") +def admin_login(): + if not check_csrf(): + return "Bad CSRF", 400 + key = throttle_key() + if throttled(key): + return "Too many attempts. Try again later.", 429 + + username = request.form.get("username","").strip() + password = request.form.get("password","") + + ok = (username == ADMIN_USERNAME and ADMIN_PASSWORD_HASH and check_password_hash(ADMIN_PASSWORD_HASH, password)) + if not ok: + bump_fail(key) + flash("Invalid username or password.", "error") + return redirect(url_for("admin_login_form")) + + session["is_admin"] = True + session.permanent = True if request.form.get("remember") == "on" else False + flash("Signed in.", "ok") + dest = request.form.get("next") or url_for("admin") + return redirect(dest) + +@app.post("/admin/logout") +@admin_required +def admin_logout(): + session.clear() + flash("Signed out.", "ok") + return redirect(url_for("admin_login_form")) + +# ----------------------------------------------------------------------------- +# Routes: admin views & actions +# ----------------------------------------------------------------------------- +@app.get("/admin") +@admin_required +def admin(): + # Filters: active (default), open, completed, deleted, all + show = request.args.get("show", "active") + where = [] + if show == "open": + where.append("(status IS NULL OR status='open') AND deleted_at IS NULL") + elif show == "completed": + where.append("status='completed' AND deleted_at IS NULL") + elif show == "deleted": + where.append("deleted_at IS NOT NULL") + elif show == "all": + pass + else: # active + where.append("deleted_at IS NULL") + + query = "SELECT * FROM quote_requests" + if where: + query += " WHERE " + " AND ".join(where) + query += " ORDER BY id DESC LIMIT 500" + + con = sqlite3.connect(DB_PATH) + con.row_factory = sqlite3.Row + cur = con.cursor() + cur.execute(query) + rows = cur.fetchall() + con.close() + + return render_template("admin.html", rows=rows, csrf=csrf_token(), show=show) + +@app.post("/admin/request//complete") +@admin_required +def mark_complete(rid: int): + if not check_csrf(): + return "Bad CSRF", 400 + con = sqlite3.connect(DB_PATH) + cur = con.cursor() + cur.execute(""" + UPDATE quote_requests + SET status='completed', completed_at=? + WHERE id=? AND deleted_at IS NULL + """, (datetime.utcnow().isoformat(), rid)) + con.commit() + con.close() + flash(f"Request #{rid} marked as completed.", "ok") + return redirect(url_for("admin", show=request.args.get("show","active"))) + +@app.post("/admin/request//delete") +@admin_required +def delete_request(rid: int): + if not check_csrf(): + return "Bad CSRF", 400 + hard = request.args.get("hard") == "1" + con = sqlite3.connect(DB_PATH) + cur = con.cursor() + if hard: + cur.execute("DELETE FROM quote_requests WHERE id=?", (rid,)) + flash(f"Request #{rid} permanently deleted.", "ok") + else: + cur.execute(""" + UPDATE quote_requests SET deleted_at=? + WHERE id=? AND deleted_at IS NULL + """, (datetime.utcnow().isoformat(), rid)) + flash(f"Request #{rid} moved to Deleted.", "ok") + con.commit() + con.close() + return redirect(url_for("admin", show=request.args.get("show","active"))) + +# Protected preview (for your client email template) +@app.get("/preview-client-email") +@admin_required +def preview_client_email(): + sample = { + "client_name": "Ben", + "project_title": "Website + Basic Admin", + "proposal_summary": "Design + dev of a 5-page brochure site with contact form and basic admin panel.", + "est_hours": 24, "hourly_rate": 95, "est_cost": 2280, + "valid_until": "November 15, 2025", + "next_steps_url": "https://example.com/pay", + "contact_email": ADMIN_EMAIL, + "company_name": "Benny's House — NetDeploy", + } + return render_template("quote_email.html", **sample) + +# ----------------------------------------------------------------------------- +# Dev server +# ----------------------------------------------------------------------------- +if __name__ == "__main__": + app.run(debug=True) + diff --git a/netdeploy/bin/Activate.ps1 b/netdeploy/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/netdeploy/bin/Activate.ps1 @@ -0,0 +1,247 @@ +<# +.Synopsis +Activate a Python virtual environment for the current PowerShell session. + +.Description +Pushes the python executable for a virtual environment to the front of the +$Env:PATH environment variable and sets the prompt to signify that you are +in a Python virtual environment. Makes use of the command line switches as +well as the `pyvenv.cfg` file values present in the virtual environment. + +.Parameter VenvDir +Path to the directory that contains the virtual environment to activate. The +default value for this is the parent of the directory that the Activate.ps1 +script is located within. + +.Parameter Prompt +The prompt prefix to display when this virtual environment is activated. By +default, this prompt is the name of the virtual environment folder (VenvDir) +surrounded by parentheses and followed by a single space (ie. '(.venv) '). + +.Example +Activate.ps1 +Activates the Python virtual environment that contains the Activate.ps1 script. + +.Example +Activate.ps1 -Verbose +Activates the Python virtual environment that contains the Activate.ps1 script, +and shows extra information about the activation as it executes. + +.Example +Activate.ps1 -VenvDir C:\Users\MyUser\Common\.venv +Activates the Python virtual environment located in the specified location. + +.Example +Activate.ps1 -Prompt "MyPython" +Activates the Python virtual environment that contains the Activate.ps1 script, +and prefixes the current prompt with the specified string (surrounded in +parentheses) while the virtual environment is active. + +.Notes +On Windows, it may be required to enable this Activate.ps1 script by setting the +execution policy for the user. You can do this by issuing the following PowerShell +command: + +PS C:\> Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser + +For more information on Execution Policies: +https://go.microsoft.com/fwlink/?LinkID=135170 + +#> +Param( + [Parameter(Mandatory = $false)] + [String] + $VenvDir, + [Parameter(Mandatory = $false)] + [String] + $Prompt +) + +<# Function declarations --------------------------------------------------- #> + +<# +.Synopsis +Remove all shell session elements added by the Activate script, including the +addition of the virtual environment's Python executable from the beginning of +the PATH variable. + +.Parameter NonDestructive +If present, do not remove this function from the global namespace for the +session. + +#> +function global:deactivate ([switch]$NonDestructive) { + # Revert to original values + + # The prior prompt: + if (Test-Path -Path Function:_OLD_VIRTUAL_PROMPT) { + Copy-Item -Path Function:_OLD_VIRTUAL_PROMPT -Destination Function:prompt + Remove-Item -Path Function:_OLD_VIRTUAL_PROMPT + } + + # The prior PYTHONHOME: + if (Test-Path -Path Env:_OLD_VIRTUAL_PYTHONHOME) { + Copy-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME -Destination Env:PYTHONHOME + Remove-Item -Path Env:_OLD_VIRTUAL_PYTHONHOME + } + + # The prior PATH: + if (Test-Path -Path Env:_OLD_VIRTUAL_PATH) { + Copy-Item -Path Env:_OLD_VIRTUAL_PATH -Destination Env:PATH + Remove-Item -Path Env:_OLD_VIRTUAL_PATH + } + + # Just remove the VIRTUAL_ENV altogether: + if (Test-Path -Path Env:VIRTUAL_ENV) { + Remove-Item -Path env:VIRTUAL_ENV + } + + # Just remove VIRTUAL_ENV_PROMPT altogether. + if (Test-Path -Path Env:VIRTUAL_ENV_PROMPT) { + Remove-Item -Path env:VIRTUAL_ENV_PROMPT + } + + # Just remove the _PYTHON_VENV_PROMPT_PREFIX altogether: + if (Get-Variable -Name "_PYTHON_VENV_PROMPT_PREFIX" -ErrorAction SilentlyContinue) { + Remove-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Scope Global -Force + } + + # Leave deactivate function in the global namespace if requested: + if (-not $NonDestructive) { + Remove-Item -Path function:deactivate + } +} + +<# +.Description +Get-PyVenvConfig parses the values from the pyvenv.cfg file located in the +given folder, and returns them in a map. + +For each line in the pyvenv.cfg file, if that line can be parsed into exactly +two strings separated by `=` (with any amount of whitespace surrounding the =) +then it is considered a `key = value` line. The left hand string is the key, +the right hand is the value. + +If the value starts with a `'` or a `"` then the first and last character is +stripped from the value before being captured. + +.Parameter ConfigDir +Path to the directory that contains the `pyvenv.cfg` file. +#> +function Get-PyVenvConfig( + [String] + $ConfigDir +) { + Write-Verbose "Given ConfigDir=$ConfigDir, obtain values in pyvenv.cfg" + + # Ensure the file exists, and issue a warning if it doesn't (but still allow the function to continue). + $pyvenvConfigPath = Join-Path -Resolve -Path $ConfigDir -ChildPath 'pyvenv.cfg' -ErrorAction Continue + + # An empty map will be returned if no config file is found. + $pyvenvConfig = @{ } + + if ($pyvenvConfigPath) { + + Write-Verbose "File exists, parse `key = value` lines" + $pyvenvConfigContent = Get-Content -Path $pyvenvConfigPath + + $pyvenvConfigContent | ForEach-Object { + $keyval = $PSItem -split "\s*=\s*", 2 + if ($keyval[0] -and $keyval[1]) { + $val = $keyval[1] + + # Remove extraneous quotations around a string value. + if ("'""".Contains($val.Substring(0, 1))) { + $val = $val.Substring(1, $val.Length - 2) + } + + $pyvenvConfig[$keyval[0]] = $val + Write-Verbose "Adding Key: '$($keyval[0])'='$val'" + } + } + } + return $pyvenvConfig +} + + +<# Begin Activate script --------------------------------------------------- #> + +# Determine the containing directory of this script +$VenvExecPath = Split-Path -Parent $MyInvocation.MyCommand.Definition +$VenvExecDir = Get-Item -Path $VenvExecPath + +Write-Verbose "Activation script is located in path: '$VenvExecPath'" +Write-Verbose "VenvExecDir Fullname: '$($VenvExecDir.FullName)" +Write-Verbose "VenvExecDir Name: '$($VenvExecDir.Name)" + +# Set values required in priority: CmdLine, ConfigFile, Default +# First, get the location of the virtual environment, it might not be +# VenvExecDir if specified on the command line. +if ($VenvDir) { + Write-Verbose "VenvDir given as parameter, using '$VenvDir' to determine values" +} +else { + Write-Verbose "VenvDir not given as a parameter, using parent directory name as VenvDir." + $VenvDir = $VenvExecDir.Parent.FullName.TrimEnd("\\/") + Write-Verbose "VenvDir=$VenvDir" +} + +# Next, read the `pyvenv.cfg` file to determine any required value such +# as `prompt`. +$pyvenvCfg = Get-PyVenvConfig -ConfigDir $VenvDir + +# Next, set the prompt from the command line, or the config file, or +# just use the name of the virtual environment folder. +if ($Prompt) { + Write-Verbose "Prompt specified as argument, using '$Prompt'" +} +else { + Write-Verbose "Prompt not specified as argument to script, checking pyvenv.cfg value" + if ($pyvenvCfg -and $pyvenvCfg['prompt']) { + Write-Verbose " Setting based on value in pyvenv.cfg='$($pyvenvCfg['prompt'])'" + $Prompt = $pyvenvCfg['prompt']; + } + else { + Write-Verbose " Setting prompt based on parent's directory's name. (Is the directory name passed to venv module when creating the virtual environment)" + Write-Verbose " Got leaf-name of $VenvDir='$(Split-Path -Path $venvDir -Leaf)'" + $Prompt = Split-Path -Path $venvDir -Leaf + } +} + +Write-Verbose "Prompt = '$Prompt'" +Write-Verbose "VenvDir='$VenvDir'" + +# Deactivate any currently active virtual environment, but leave the +# deactivate function in place. +deactivate -nondestructive + +# Now set the environment variable VIRTUAL_ENV, used by many tools to determine +# that there is an activated venv. +$env:VIRTUAL_ENV = $VenvDir + +if (-not $Env:VIRTUAL_ENV_DISABLE_PROMPT) { + + Write-Verbose "Setting prompt to '$Prompt'" + + # Set the prompt to include the env name + # Make sure _OLD_VIRTUAL_PROMPT is global + function global:_OLD_VIRTUAL_PROMPT { "" } + Copy-Item -Path function:prompt -Destination function:_OLD_VIRTUAL_PROMPT + New-Variable -Name _PYTHON_VENV_PROMPT_PREFIX -Description "Python virtual environment prompt prefix" -Scope Global -Option ReadOnly -Visibility Public -Value $Prompt + + function global:prompt { + Write-Host -NoNewline -ForegroundColor Green "($_PYTHON_VENV_PROMPT_PREFIX) " + _OLD_VIRTUAL_PROMPT + } + $env:VIRTUAL_ENV_PROMPT = $Prompt +} + +# Clear PYTHONHOME +if (Test-Path -Path Env:PYTHONHOME) { + Copy-Item -Path Env:PYTHONHOME -Destination Env:_OLD_VIRTUAL_PYTHONHOME + Remove-Item -Path Env:PYTHONHOME +} + +# Add the venv to the PATH +Copy-Item -Path Env:PATH -Destination Env:_OLD_VIRTUAL_PATH +$Env:PATH = "$VenvExecDir$([System.IO.Path]::PathSeparator)$Env:PATH" diff --git a/netdeploy/bin/activate b/netdeploy/bin/activate new file mode 100644 index 0000000..30e34e8 --- /dev/null +++ b/netdeploy/bin/activate @@ -0,0 +1,69 @@ +# This file must be used with "source bin/activate" *from bash* +# you cannot run it directly + +deactivate () { + # reset old environment variables + if [ -n "${_OLD_VIRTUAL_PATH:-}" ] ; then + PATH="${_OLD_VIRTUAL_PATH:-}" + export PATH + unset _OLD_VIRTUAL_PATH + fi + if [ -n "${_OLD_VIRTUAL_PYTHONHOME:-}" ] ; then + PYTHONHOME="${_OLD_VIRTUAL_PYTHONHOME:-}" + export PYTHONHOME + unset _OLD_VIRTUAL_PYTHONHOME + fi + + # This should detect bash and zsh, which have a hash command that must + # be called to get it to forget past commands. Without forgetting + # past commands the $PATH changes we made may not be respected + if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null + fi + + if [ -n "${_OLD_VIRTUAL_PS1:-}" ] ; then + PS1="${_OLD_VIRTUAL_PS1:-}" + export PS1 + unset _OLD_VIRTUAL_PS1 + fi + + unset VIRTUAL_ENV + unset VIRTUAL_ENV_PROMPT + if [ ! "${1:-}" = "nondestructive" ] ; then + # Self destruct! + unset -f deactivate + fi +} + +# unset irrelevant variables +deactivate nondestructive + +VIRTUAL_ENV=/var/www/netdeploy/netdeploy +export VIRTUAL_ENV + +_OLD_VIRTUAL_PATH="$PATH" +PATH="$VIRTUAL_ENV/"bin":$PATH" +export PATH + +# unset PYTHONHOME if set +# this will fail if PYTHONHOME is set to the empty string (which is bad anyway) +# could use `if (set -u; : $PYTHONHOME) ;` in bash +if [ -n "${PYTHONHOME:-}" ] ; then + _OLD_VIRTUAL_PYTHONHOME="${PYTHONHOME:-}" + unset PYTHONHOME +fi + +if [ -z "${VIRTUAL_ENV_DISABLE_PROMPT:-}" ] ; then + _OLD_VIRTUAL_PS1="${PS1:-}" + PS1='(netdeploy) '"${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT='(netdeploy) ' + export VIRTUAL_ENV_PROMPT +fi + +# This should detect bash and zsh, which have a hash command that must +# be called to get it to forget past commands. Without forgetting +# past commands the $PATH changes we made may not be respected +if [ -n "${BASH:-}" -o -n "${ZSH_VERSION:-}" ] ; then + hash -r 2> /dev/null +fi diff --git a/netdeploy/bin/activate.csh b/netdeploy/bin/activate.csh new file mode 100644 index 0000000..b87c739 --- /dev/null +++ b/netdeploy/bin/activate.csh @@ -0,0 +1,26 @@ +# This file must be used with "source bin/activate.csh" *from csh*. +# You cannot run it directly. +# Created by Davide Di Blasi . +# Ported to Python 3.3 venv by Andrew Svetlov + +alias deactivate 'test $?_OLD_VIRTUAL_PATH != 0 && setenv PATH "$_OLD_VIRTUAL_PATH" && unset _OLD_VIRTUAL_PATH; rehash; test $?_OLD_VIRTUAL_PROMPT != 0 && set prompt="$_OLD_VIRTUAL_PROMPT" && unset _OLD_VIRTUAL_PROMPT; unsetenv VIRTUAL_ENV; unsetenv VIRTUAL_ENV_PROMPT; test "\!:*" != "nondestructive" && unalias deactivate' + +# Unset irrelevant variables. +deactivate nondestructive + +setenv VIRTUAL_ENV /var/www/netdeploy/netdeploy + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/"bin":$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = '(netdeploy) '"$prompt" + setenv VIRTUAL_ENV_PROMPT '(netdeploy) ' +endif + +alias pydoc python -m pydoc + +rehash diff --git a/netdeploy/bin/activate.fish b/netdeploy/bin/activate.fish new file mode 100644 index 0000000..4976793 --- /dev/null +++ b/netdeploy/bin/activate.fish @@ -0,0 +1,69 @@ +# This file must be used with "source /bin/activate.fish" *from fish* +# (https://fishshell.com/); you cannot run it directly. + +function deactivate -d "Exit virtual environment and return to normal shell environment" + # reset old environment variables + if test -n "$_OLD_VIRTUAL_PATH" + set -gx PATH $_OLD_VIRTUAL_PATH + set -e _OLD_VIRTUAL_PATH + end + if test -n "$_OLD_VIRTUAL_PYTHONHOME" + set -gx PYTHONHOME $_OLD_VIRTUAL_PYTHONHOME + set -e _OLD_VIRTUAL_PYTHONHOME + end + + if test -n "$_OLD_FISH_PROMPT_OVERRIDE" + set -e _OLD_FISH_PROMPT_OVERRIDE + # prevents error when using nested fish instances (Issue #93858) + if functions -q _old_fish_prompt + functions -e fish_prompt + functions -c _old_fish_prompt fish_prompt + functions -e _old_fish_prompt + end + end + + set -e VIRTUAL_ENV + set -e VIRTUAL_ENV_PROMPT + if test "$argv[1]" != "nondestructive" + # Self-destruct! + functions -e deactivate + end +end + +# Unset irrelevant variables. +deactivate nondestructive + +set -gx VIRTUAL_ENV /var/www/netdeploy/netdeploy + +set -gx _OLD_VIRTUAL_PATH $PATH +set -gx PATH "$VIRTUAL_ENV/"bin $PATH + +# Unset PYTHONHOME if set. +if set -q PYTHONHOME + set -gx _OLD_VIRTUAL_PYTHONHOME $PYTHONHOME + set -e PYTHONHOME +end + +if test -z "$VIRTUAL_ENV_DISABLE_PROMPT" + # fish uses a function instead of an env var to generate the prompt. + + # Save the current fish_prompt function as the function _old_fish_prompt. + functions -c fish_prompt _old_fish_prompt + + # With the original prompt function renamed, we can override with our own. + function fish_prompt + # Save the return status of the last command. + set -l old_status $status + + # Output the venv prompt; color taken from the blue of the Python logo. + printf "%s%s%s" (set_color 4B8BBE) '(netdeploy) ' (set_color normal) + + # Restore the return status of the previous command. + echo "exit $old_status" | . + # Output the original/"old" prompt. + _old_fish_prompt + end + + set -gx _OLD_FISH_PROMPT_OVERRIDE "$VIRTUAL_ENV" + set -gx VIRTUAL_ENV_PROMPT '(netdeploy) ' +end diff --git a/netdeploy/bin/dotenv b/netdeploy/bin/dotenv new file mode 100755 index 0000000..7a2e57b --- /dev/null +++ b/netdeploy/bin/dotenv @@ -0,0 +1,8 @@ +#!/var/www/netdeploy/netdeploy/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from dotenv.__main__ import cli +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(cli()) diff --git a/netdeploy/bin/flask b/netdeploy/bin/flask new file mode 100755 index 0000000..af25a02 --- /dev/null +++ b/netdeploy/bin/flask @@ -0,0 +1,8 @@ +#!/var/www/netdeploy/netdeploy/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from flask.cli import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/netdeploy/bin/gunicorn b/netdeploy/bin/gunicorn new file mode 100755 index 0000000..4a5df30 --- /dev/null +++ b/netdeploy/bin/gunicorn @@ -0,0 +1,8 @@ +#!/var/www/netdeploy/netdeploy/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from gunicorn.app.wsgiapp import run +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(run()) diff --git a/netdeploy/bin/pip b/netdeploy/bin/pip new file mode 100755 index 0000000..cddeadb --- /dev/null +++ b/netdeploy/bin/pip @@ -0,0 +1,8 @@ +#!/var/www/netdeploy/netdeploy/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/netdeploy/bin/pip3 b/netdeploy/bin/pip3 new file mode 100755 index 0000000..cddeadb --- /dev/null +++ b/netdeploy/bin/pip3 @@ -0,0 +1,8 @@ +#!/var/www/netdeploy/netdeploy/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/netdeploy/bin/pip3.11 b/netdeploy/bin/pip3.11 new file mode 100755 index 0000000..cddeadb --- /dev/null +++ b/netdeploy/bin/pip3.11 @@ -0,0 +1,8 @@ +#!/var/www/netdeploy/netdeploy/bin/python3 +# -*- coding: utf-8 -*- +import re +import sys +from pip._internal.cli.main import main +if __name__ == '__main__': + sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0]) + sys.exit(main()) diff --git a/netdeploy/bin/python b/netdeploy/bin/python new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/netdeploy/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/netdeploy/bin/python3 b/netdeploy/bin/python3 new file mode 120000 index 0000000..ae65fda --- /dev/null +++ b/netdeploy/bin/python3 @@ -0,0 +1 @@ +/usr/bin/python3 \ No newline at end of file diff --git a/netdeploy/bin/python3.11 b/netdeploy/bin/python3.11 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/netdeploy/bin/python3.11 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/netdeploy/include/site/python3.11/greenlet/greenlet.h b/netdeploy/include/site/python3.11/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/netdeploy/include/site/python3.11/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/netdeploy/lib/python3.11/site-packages/_distutils_hack/__init__.py b/netdeploy/lib/python3.11/site-packages/_distutils_hack/__init__.py new file mode 100644 index 0000000..f987a53 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/_distutils_hack/__init__.py @@ -0,0 +1,222 @@ +# don't import any costly modules +import sys +import os + + +is_pypy = '__pypy__' in sys.builtin_module_names + + +def warn_distutils_present(): + if 'distutils' not in sys.modules: + return + if is_pypy and sys.version_info < (3, 7): + # PyPy for 3.6 unconditionally imports distutils, so bypass the warning + # https://foss.heptapod.net/pypy/pypy/-/blob/be829135bc0d758997b3566062999ee8b23872b4/lib-python/3/site.py#L250 + return + import warnings + + warnings.warn( + "Distutils was imported before Setuptools, but importing Setuptools " + "also replaces the `distutils` module in `sys.modules`. This may lead " + "to undesirable behaviors or errors. To avoid these issues, avoid " + "using distutils directly, ensure that setuptools is installed in the " + "traditional way (e.g. not an editable install), and/or make sure " + "that setuptools is always imported before distutils." + ) + + +def clear_distutils(): + if 'distutils' not in sys.modules: + return + import warnings + + warnings.warn("Setuptools is replacing distutils.") + mods = [ + name + for name in sys.modules + if name == "distutils" or name.startswith("distutils.") + ] + for name in mods: + del sys.modules[name] + + +def enabled(): + """ + Allow selection of distutils by environment variable. + """ + which = os.environ.get('SETUPTOOLS_USE_DISTUTILS', 'local') + return which == 'local' + + +def ensure_local_distutils(): + import importlib + + clear_distutils() + + # With the DistutilsMetaFinder in place, + # perform an import to cause distutils to be + # loaded from setuptools._distutils. Ref #2906. + with shim(): + importlib.import_module('distutils') + + # check that submodules load as expected + core = importlib.import_module('distutils.core') + assert '_distutils' in core.__file__, core.__file__ + assert 'setuptools._distutils.log' not in sys.modules + + +def do_override(): + """ + Ensure that the local copy of distutils is preferred over stdlib. + + See https://github.com/pypa/setuptools/issues/417#issuecomment-392298401 + for more motivation. + """ + if enabled(): + warn_distutils_present() + ensure_local_distutils() + + +class _TrivialRe: + def __init__(self, *patterns): + self._patterns = patterns + + def match(self, string): + return all(pat in string for pat in self._patterns) + + +class DistutilsMetaFinder: + def find_spec(self, fullname, path, target=None): + # optimization: only consider top level modules and those + # found in the CPython test suite. + if path is not None and not fullname.startswith('test.'): + return + + method_name = 'spec_for_{fullname}'.format(**locals()) + method = getattr(self, method_name, lambda: None) + return method() + + def spec_for_distutils(self): + if self.is_cpython(): + return + + import importlib + import importlib.abc + import importlib.util + + try: + mod = importlib.import_module('setuptools._distutils') + except Exception: + # There are a couple of cases where setuptools._distutils + # may not be present: + # - An older Setuptools without a local distutils is + # taking precedence. Ref #2957. + # - Path manipulation during sitecustomize removes + # setuptools from the path but only after the hook + # has been loaded. Ref #2980. + # In either case, fall back to stdlib behavior. + return + + class DistutilsLoader(importlib.abc.Loader): + def create_module(self, spec): + mod.__name__ = 'distutils' + return mod + + def exec_module(self, module): + pass + + return importlib.util.spec_from_loader( + 'distutils', DistutilsLoader(), origin=mod.__file__ + ) + + @staticmethod + def is_cpython(): + """ + Suppress supplying distutils for CPython (build and tests). + Ref #2965 and #3007. + """ + return os.path.isfile('pybuilddir.txt') + + def spec_for_pip(self): + """ + Ensure stdlib distutils when running under pip. + See pypa/pip#8761 for rationale. + """ + if self.pip_imported_during_build(): + return + clear_distutils() + self.spec_for_distutils = lambda: None + + @classmethod + def pip_imported_during_build(cls): + """ + Detect if pip is being imported in a build script. Ref #2355. + """ + import traceback + + return any( + cls.frame_file_is_setup(frame) for frame, line in traceback.walk_stack(None) + ) + + @staticmethod + def frame_file_is_setup(frame): + """ + Return True if the indicated frame suggests a setup.py file. + """ + # some frames may not have __file__ (#2940) + return frame.f_globals.get('__file__', '').endswith('setup.py') + + def spec_for_sensitive_tests(self): + """ + Ensure stdlib distutils when running select tests under CPython. + + python/cpython#91169 + """ + clear_distutils() + self.spec_for_distutils = lambda: None + + sensitive_tests = ( + [ + 'test.test_distutils', + 'test.test_peg_generator', + 'test.test_importlib', + ] + if sys.version_info < (3, 10) + else [ + 'test.test_distutils', + ] + ) + + +for name in DistutilsMetaFinder.sensitive_tests: + setattr( + DistutilsMetaFinder, + f'spec_for_{name}', + DistutilsMetaFinder.spec_for_sensitive_tests, + ) + + +DISTUTILS_FINDER = DistutilsMetaFinder() + + +def add_shim(): + DISTUTILS_FINDER in sys.meta_path or insert_shim() + + +class shim: + def __enter__(self): + insert_shim() + + def __exit__(self, exc, value, tb): + remove_shim() + + +def insert_shim(): + sys.meta_path.insert(0, DISTUTILS_FINDER) + + +def remove_shim(): + try: + sys.meta_path.remove(DISTUTILS_FINDER) + except ValueError: + pass diff --git a/netdeploy/lib/python3.11/site-packages/_distutils_hack/override.py b/netdeploy/lib/python3.11/site-packages/_distutils_hack/override.py new file mode 100644 index 0000000..2cc433a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/_distutils_hack/override.py @@ -0,0 +1 @@ +__import__('_distutils_hack').do_override() diff --git a/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/INSTALLER b/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/LICENSE.txt b/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/LICENSE.txt new file mode 100644 index 0000000..79c9825 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/LICENSE.txt @@ -0,0 +1,20 @@ +Copyright 2010 Jason Kirtland + +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/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/METADATA b/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/METADATA new file mode 100644 index 0000000..6d343f5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/METADATA @@ -0,0 +1,60 @@ +Metadata-Version: 2.3 +Name: blinker +Version: 1.9.0 +Summary: Fast, simple object-to-object and broadcast signaling +Author: Jason Kirtland +Maintainer-email: Pallets Ecosystem +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python +Classifier: Typing :: Typed +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://blinker.readthedocs.io +Project-URL: Source, https://github.com/pallets-eco/blinker/ + +# Blinker + +Blinker provides a fast dispatching system that allows any number of +interested parties to subscribe to events, or "signals". + + +## Pallets Community Ecosystem + +> [!IMPORTANT]\ +> This project is part of the Pallets Community Ecosystem. Pallets is the open +> source organization that maintains Flask; Pallets-Eco enables community +> maintenance of related projects. If you are interested in helping maintain +> this project, please reach out on [the Pallets Discord server][discord]. +> +> [discord]: https://discord.gg/pallets + + +## Example + +Signal receivers can subscribe to specific senders or receive signals +sent by any sender. + +```pycon +>>> from blinker import signal +>>> started = signal('round-started') +>>> def each(round): +... print(f"Round {round}") +... +>>> started.connect(each) + +>>> def round_two(round): +... print("This is round two.") +... +>>> started.connect(round_two, sender=2) + +>>> for round in range(1, 4): +... started.send(round) +... +Round 1! +Round 2! +This is round two. +Round 3! +``` + diff --git a/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/RECORD b/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/RECORD new file mode 100644 index 0000000..52d1667 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/RECORD @@ -0,0 +1,12 @@ +blinker-1.9.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +blinker-1.9.0.dist-info/LICENSE.txt,sha256=nrc6HzhZekqhcCXSrhvjg5Ykx5XphdTw6Xac4p-spGc,1054 +blinker-1.9.0.dist-info/METADATA,sha256=uIRiM8wjjbHkCtbCyTvctU37IAZk0kEe5kxAld1dvzA,1633 +blinker-1.9.0.dist-info/RECORD,, +blinker-1.9.0.dist-info/WHEEL,sha256=CpUCUxeHQbRN5UGRQHYRJorO5Af-Qy_fHMctcQ8DSGI,82 +blinker/__init__.py,sha256=I2EdZqpy4LyjX17Hn1yzJGWCjeLaVaPzsMgHkLfj_cQ,317 +blinker/__pycache__/__init__.cpython-311.pyc,, +blinker/__pycache__/_utilities.cpython-311.pyc,, +blinker/__pycache__/base.cpython-311.pyc,, +blinker/_utilities.py,sha256=0J7eeXXTUx0Ivf8asfpx0ycVkp0Eqfqnj117x2mYX9E,1675 +blinker/base.py,sha256=QpDuvXXcwJF49lUBcH5BiST46Rz9wSG7VW_p7N_027M,19132 +blinker/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 diff --git a/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/WHEEL b/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/WHEEL new file mode 100644 index 0000000..e3c6fee --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/blinker-1.9.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.10.1 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/netdeploy/lib/python3.11/site-packages/blinker/__init__.py b/netdeploy/lib/python3.11/site-packages/blinker/__init__.py new file mode 100644 index 0000000..1772fa4 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/blinker/__init__.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from .base import ANY +from .base import default_namespace +from .base import NamedSignal +from .base import Namespace +from .base import Signal +from .base import signal + +__all__ = [ + "ANY", + "default_namespace", + "NamedSignal", + "Namespace", + "Signal", + "signal", +] diff --git a/netdeploy/lib/python3.11/site-packages/blinker/_utilities.py b/netdeploy/lib/python3.11/site-packages/blinker/_utilities.py new file mode 100644 index 0000000..000c902 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/blinker/_utilities.py @@ -0,0 +1,64 @@ +from __future__ import annotations + +import collections.abc as c +import inspect +import typing as t +from weakref import ref +from weakref import WeakMethod + +T = t.TypeVar("T") + + +class Symbol: + """A constant symbol, nicer than ``object()``. Repeated calls return the + same instance. + + >>> Symbol('foo') is Symbol('foo') + True + >>> Symbol('foo') + foo + """ + + symbols: t.ClassVar[dict[str, Symbol]] = {} + + def __new__(cls, name: str) -> Symbol: + if name in cls.symbols: + return cls.symbols[name] + + obj = super().__new__(cls) + cls.symbols[name] = obj + return obj + + def __init__(self, name: str) -> None: + self.name = name + + def __repr__(self) -> str: + return self.name + + def __getnewargs__(self) -> tuple[t.Any, ...]: + return (self.name,) + + +def make_id(obj: object) -> c.Hashable: + """Get a stable identifier for a receiver or sender, to be used as a dict + key or in a set. + """ + if inspect.ismethod(obj): + # The id of a bound method is not stable, but the id of the unbound + # function and instance are. + return id(obj.__func__), id(obj.__self__) + + if isinstance(obj, (str, int)): + # Instances with the same value always compare equal and have the same + # hash, even if the id may change. + return obj + + # Assume other types are not hashable but will always be the same instance. + return id(obj) + + +def make_ref(obj: T, callback: c.Callable[[ref[T]], None] | None = None) -> ref[T]: + if inspect.ismethod(obj): + return WeakMethod(obj, callback) # type: ignore[arg-type, return-value] + + return ref(obj, callback) diff --git a/netdeploy/lib/python3.11/site-packages/blinker/base.py b/netdeploy/lib/python3.11/site-packages/blinker/base.py new file mode 100644 index 0000000..d051b94 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/blinker/base.py @@ -0,0 +1,512 @@ +from __future__ import annotations + +import collections.abc as c +import sys +import typing as t +import weakref +from collections import defaultdict +from contextlib import contextmanager +from functools import cached_property +from inspect import iscoroutinefunction + +from ._utilities import make_id +from ._utilities import make_ref +from ._utilities import Symbol + +F = t.TypeVar("F", bound=c.Callable[..., t.Any]) + +ANY = Symbol("ANY") +"""Symbol for "any sender".""" + +ANY_ID = 0 + + +class Signal: + """A notification emitter. + + :param doc: The docstring for the signal. + """ + + ANY = ANY + """An alias for the :data:`~blinker.ANY` sender symbol.""" + + set_class: type[set[t.Any]] = set + """The set class to use for tracking connected receivers and senders. + Python's ``set`` is unordered. If receivers must be dispatched in the order + they were connected, an ordered set implementation can be used. + + .. versionadded:: 1.7 + """ + + @cached_property + def receiver_connected(self) -> Signal: + """Emitted at the end of each :meth:`connect` call. + + The signal sender is the signal instance, and the :meth:`connect` + arguments are passed through: ``receiver``, ``sender``, and ``weak``. + + .. versionadded:: 1.2 + """ + return Signal(doc="Emitted after a receiver connects.") + + @cached_property + def receiver_disconnected(self) -> Signal: + """Emitted at the end of each :meth:`disconnect` call. + + The sender is the signal instance, and the :meth:`disconnect` arguments + are passed through: ``receiver`` and ``sender``. + + This signal is emitted **only** when :meth:`disconnect` is called + explicitly. This signal cannot be emitted by an automatic disconnect + when a weakly referenced receiver or sender goes out of scope, as the + instance is no longer be available to be used as the sender for this + signal. + + An alternative approach is available by subscribing to + :attr:`receiver_connected` and setting up a custom weakref cleanup + callback on weak receivers and senders. + + .. versionadded:: 1.2 + """ + return Signal(doc="Emitted after a receiver disconnects.") + + def __init__(self, doc: str | None = None) -> None: + if doc: + self.__doc__ = doc + + self.receivers: dict[ + t.Any, weakref.ref[c.Callable[..., t.Any]] | c.Callable[..., t.Any] + ] = {} + """The map of connected receivers. Useful to quickly check if any + receivers are connected to the signal: ``if s.receivers:``. The + structure and data is not part of the public API, but checking its + boolean value is. + """ + + self.is_muted: bool = False + self._by_receiver: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) + self._by_sender: dict[t.Any, set[t.Any]] = defaultdict(self.set_class) + self._weak_senders: dict[t.Any, weakref.ref[t.Any]] = {} + + def connect(self, receiver: F, sender: t.Any = ANY, weak: bool = True) -> F: + """Connect ``receiver`` to be called when the signal is sent by + ``sender``. + + :param receiver: The callable to call when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument + along with any extra keyword arguments. + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. A receiver may be connected + to multiple senders by calling :meth:`connect` multiple times. + :param weak: Track the receiver with a :mod:`weakref`. The receiver will + be automatically disconnected when it is garbage collected. When + connecting a receiver defined within a function, set to ``False``, + otherwise it will be disconnected when the function scope ends. + """ + receiver_id = make_id(receiver) + sender_id = ANY_ID if sender is ANY else make_id(sender) + + if weak: + self.receivers[receiver_id] = make_ref( + receiver, self._make_cleanup_receiver(receiver_id) + ) + else: + self.receivers[receiver_id] = receiver + + self._by_sender[sender_id].add(receiver_id) + self._by_receiver[receiver_id].add(sender_id) + + if sender is not ANY and sender_id not in self._weak_senders: + # store a cleanup for weakref-able senders + try: + self._weak_senders[sender_id] = make_ref( + sender, self._make_cleanup_sender(sender_id) + ) + except TypeError: + pass + + if "receiver_connected" in self.__dict__ and self.receiver_connected.receivers: + try: + self.receiver_connected.send( + self, receiver=receiver, sender=sender, weak=weak + ) + except TypeError: + # TODO no explanation or test for this + self.disconnect(receiver, sender) + raise + + return receiver + + def connect_via(self, sender: t.Any, weak: bool = False) -> c.Callable[[F], F]: + """Connect the decorated function to be called when the signal is sent + by ``sender``. + + The decorated function will be called when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument along + with any extra keyword arguments. + + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. A receiver may be connected + to multiple senders by calling :meth:`connect` multiple times. + :param weak: Track the receiver with a :mod:`weakref`. The receiver will + be automatically disconnected when it is garbage collected. When + connecting a receiver defined within a function, set to ``False``, + otherwise it will be disconnected when the function scope ends.= + + .. versionadded:: 1.1 + """ + + def decorator(fn: F) -> F: + self.connect(fn, sender, weak) + return fn + + return decorator + + @contextmanager + def connected_to( + self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY + ) -> c.Generator[None, None, None]: + """A context manager that temporarily connects ``receiver`` to the + signal while a ``with`` block executes. When the block exits, the + receiver is disconnected. Useful for tests. + + :param receiver: The callable to call when :meth:`send` is called with + the given ``sender``, passing ``sender`` as a positional argument + along with any extra keyword arguments. + :param sender: Any object or :data:`ANY`. ``receiver`` will only be + called when :meth:`send` is called with this sender. If ``ANY``, the + receiver will be called for any sender. + + .. versionadded:: 1.1 + """ + self.connect(receiver, sender=sender, weak=False) + + try: + yield None + finally: + self.disconnect(receiver) + + @contextmanager + def muted(self) -> c.Generator[None, None, None]: + """A context manager that temporarily disables the signal. No receivers + will be called if the signal is sent, until the ``with`` block exits. + Useful for tests. + """ + self.is_muted = True + + try: + yield None + finally: + self.is_muted = False + + def send( + self, + sender: t.Any | None = None, + /, + *, + _async_wrapper: c.Callable[ + [c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]]], c.Callable[..., t.Any] + ] + | None = None, + **kwargs: t.Any, + ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: + """Call all receivers that are connected to the given ``sender`` + or :data:`ANY`. Each receiver is called with ``sender`` as a positional + argument along with any extra keyword arguments. Return a list of + ``(receiver, return value)`` tuples. + + The order receivers are called is undefined, but can be influenced by + setting :attr:`set_class`. + + If a receiver raises an exception, that exception will propagate up. + This makes debugging straightforward, with an assumption that correctly + implemented receivers will not raise. + + :param sender: Call receivers connected to this sender, in addition to + those connected to :data:`ANY`. + :param _async_wrapper: Will be called on any receivers that are async + coroutines to turn them into sync callables. For example, could run + the receiver with an event loop. + :param kwargs: Extra keyword arguments to pass to each receiver. + + .. versionchanged:: 1.7 + Added the ``_async_wrapper`` argument. + """ + if self.is_muted: + return [] + + results = [] + + for receiver in self.receivers_for(sender): + if iscoroutinefunction(receiver): + if _async_wrapper is None: + raise RuntimeError("Cannot send to a coroutine function.") + + result = _async_wrapper(receiver)(sender, **kwargs) + else: + result = receiver(sender, **kwargs) + + results.append((receiver, result)) + + return results + + async def send_async( + self, + sender: t.Any | None = None, + /, + *, + _sync_wrapper: c.Callable[ + [c.Callable[..., t.Any]], c.Callable[..., c.Coroutine[t.Any, t.Any, t.Any]] + ] + | None = None, + **kwargs: t.Any, + ) -> list[tuple[c.Callable[..., t.Any], t.Any]]: + """Await all receivers that are connected to the given ``sender`` + or :data:`ANY`. Each receiver is called with ``sender`` as a positional + argument along with any extra keyword arguments. Return a list of + ``(receiver, return value)`` tuples. + + The order receivers are called is undefined, but can be influenced by + setting :attr:`set_class`. + + If a receiver raises an exception, that exception will propagate up. + This makes debugging straightforward, with an assumption that correctly + implemented receivers will not raise. + + :param sender: Call receivers connected to this sender, in addition to + those connected to :data:`ANY`. + :param _sync_wrapper: Will be called on any receivers that are sync + callables to turn them into async coroutines. For example, + could call the receiver in a thread. + :param kwargs: Extra keyword arguments to pass to each receiver. + + .. versionadded:: 1.7 + """ + if self.is_muted: + return [] + + results = [] + + for receiver in self.receivers_for(sender): + if not iscoroutinefunction(receiver): + if _sync_wrapper is None: + raise RuntimeError("Cannot send to a non-coroutine function.") + + result = await _sync_wrapper(receiver)(sender, **kwargs) + else: + result = await receiver(sender, **kwargs) + + results.append((receiver, result)) + + return results + + def has_receivers_for(self, sender: t.Any) -> bool: + """Check if there is at least one receiver that will be called with the + given ``sender``. A receiver connected to :data:`ANY` will always be + called, regardless of sender. Does not check if weakly referenced + receivers are still live. See :meth:`receivers_for` for a stronger + search. + + :param sender: Check for receivers connected to this sender, in addition + to those connected to :data:`ANY`. + """ + if not self.receivers: + return False + + if self._by_sender[ANY_ID]: + return True + + if sender is ANY: + return False + + return make_id(sender) in self._by_sender + + def receivers_for( + self, sender: t.Any + ) -> c.Generator[c.Callable[..., t.Any], None, None]: + """Yield each receiver to be called for ``sender``, in addition to those + to be called for :data:`ANY`. Weakly referenced receivers that are not + live will be disconnected and skipped. + + :param sender: Yield receivers connected to this sender, in addition + to those connected to :data:`ANY`. + """ + # TODO: test receivers_for(ANY) + if not self.receivers: + return + + sender_id = make_id(sender) + + if sender_id in self._by_sender: + ids = self._by_sender[ANY_ID] | self._by_sender[sender_id] + else: + ids = self._by_sender[ANY_ID].copy() + + for receiver_id in ids: + receiver = self.receivers.get(receiver_id) + + if receiver is None: + continue + + if isinstance(receiver, weakref.ref): + strong = receiver() + + if strong is None: + self._disconnect(receiver_id, ANY_ID) + continue + + yield strong + else: + yield receiver + + def disconnect(self, receiver: c.Callable[..., t.Any], sender: t.Any = ANY) -> None: + """Disconnect ``receiver`` from being called when the signal is sent by + ``sender``. + + :param receiver: A connected receiver callable. + :param sender: Disconnect from only this sender. By default, disconnect + from all senders. + """ + sender_id: c.Hashable + + if sender is ANY: + sender_id = ANY_ID + else: + sender_id = make_id(sender) + + receiver_id = make_id(receiver) + self._disconnect(receiver_id, sender_id) + + if ( + "receiver_disconnected" in self.__dict__ + and self.receiver_disconnected.receivers + ): + self.receiver_disconnected.send(self, receiver=receiver, sender=sender) + + def _disconnect(self, receiver_id: c.Hashable, sender_id: c.Hashable) -> None: + if sender_id == ANY_ID: + if self._by_receiver.pop(receiver_id, None) is not None: + for bucket in self._by_sender.values(): + bucket.discard(receiver_id) + + self.receivers.pop(receiver_id, None) + else: + self._by_sender[sender_id].discard(receiver_id) + self._by_receiver[receiver_id].discard(sender_id) + + def _make_cleanup_receiver( + self, receiver_id: c.Hashable + ) -> c.Callable[[weakref.ref[c.Callable[..., t.Any]]], None]: + """Create a callback function to disconnect a weakly referenced + receiver when it is garbage collected. + """ + + def cleanup(ref: weakref.ref[c.Callable[..., t.Any]]) -> None: + # If the interpreter is shutting down, disconnecting can result in a + # weird ignored exception. Don't call it in that case. + if not sys.is_finalizing(): + self._disconnect(receiver_id, ANY_ID) + + return cleanup + + def _make_cleanup_sender( + self, sender_id: c.Hashable + ) -> c.Callable[[weakref.ref[t.Any]], None]: + """Create a callback function to disconnect all receivers for a weakly + referenced sender when it is garbage collected. + """ + assert sender_id != ANY_ID + + def cleanup(ref: weakref.ref[t.Any]) -> None: + self._weak_senders.pop(sender_id, None) + + for receiver_id in self._by_sender.pop(sender_id, ()): + self._by_receiver[receiver_id].discard(sender_id) + + return cleanup + + def _cleanup_bookkeeping(self) -> None: + """Prune unused sender/receiver bookkeeping. Not threadsafe. + + Connecting & disconnecting leaves behind a small amount of bookkeeping + data. Typical workloads using Blinker, for example in most web apps, + Flask, CLI scripts, etc., are not adversely affected by this + bookkeeping. + + With a long-running process performing dynamic signal routing with high + volume, e.g. connecting to function closures, senders are all unique + object instances. Doing all of this over and over may cause memory usage + to grow due to extraneous bookkeeping. (An empty ``set`` for each stale + sender/receiver pair.) + + This method will prune that bookkeeping away, with the caveat that such + pruning is not threadsafe. The risk is that cleanup of a fully + disconnected receiver/sender pair occurs while another thread is + connecting that same pair. If you are in the highly dynamic, unique + receiver/sender situation that has lead you to this method, that failure + mode is perhaps not a big deal for you. + """ + for mapping in (self._by_sender, self._by_receiver): + for ident, bucket in list(mapping.items()): + if not bucket: + mapping.pop(ident, None) + + def _clear_state(self) -> None: + """Disconnect all receivers and senders. Useful for tests.""" + self._weak_senders.clear() + self.receivers.clear() + self._by_sender.clear() + self._by_receiver.clear() + + +class NamedSignal(Signal): + """A named generic notification emitter. The name is not used by the signal + itself, but matches the key in the :class:`Namespace` that it belongs to. + + :param name: The name of the signal within the namespace. + :param doc: The docstring for the signal. + """ + + def __init__(self, name: str, doc: str | None = None) -> None: + super().__init__(doc) + + #: The name of this signal. + self.name: str = name + + def __repr__(self) -> str: + base = super().__repr__() + return f"{base[:-1]}; {self.name!r}>" # noqa: E702 + + +class Namespace(dict[str, NamedSignal]): + """A dict mapping names to signals.""" + + def signal(self, name: str, doc: str | None = None) -> NamedSignal: + """Return the :class:`NamedSignal` for the given ``name``, creating it + if required. Repeated calls with the same name return the same signal. + + :param name: The name of the signal. + :param doc: The docstring of the signal. + """ + if name not in self: + self[name] = NamedSignal(name, doc) + + return self[name] + + +class _PNamespaceSignal(t.Protocol): + def __call__(self, name: str, doc: str | None = None) -> NamedSignal: ... + + +default_namespace: Namespace = Namespace() +"""A default :class:`Namespace` for creating named signals. :func:`signal` +creates a :class:`NamedSignal` in this namespace. +""" + +signal: _PNamespaceSignal = default_namespace.signal +"""Return a :class:`NamedSignal` in :data:`default_namespace` with the given +``name``, creating it if required. Repeated calls with the same name return the +same signal. +""" diff --git a/netdeploy/lib/python3.11/site-packages/blinker/py.typed b/netdeploy/lib/python3.11/site-packages/blinker/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/INSTALLER b/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/METADATA b/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/METADATA new file mode 100644 index 0000000..534eb57 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/METADATA @@ -0,0 +1,84 @@ +Metadata-Version: 2.4 +Name: click +Version: 8.3.0 +Summary: Composable command line interface toolkit +Maintainer-email: Pallets +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-Expression: BSD-3-Clause +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Typing :: Typed +License-File: LICENSE.txt +Requires-Dist: colorama; platform_system == 'Windows' +Project-URL: Changes, https://click.palletsprojects.com/page/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://click.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/click/ + +
+ +# Click + +Click is a Python package for creating beautiful command line interfaces +in a composable way with as little code as necessary. It's the "Command +Line Interface Creation Kit". It's highly configurable but comes with +sensible defaults out of the box. + +It aims to make the process of writing command line tools quick and fun +while also preventing any frustration caused by the inability to +implement an intended CLI API. + +Click in three points: + +- Arbitrary nesting of commands +- Automatic help page generation +- Supports lazy loading of subcommands at runtime + + +## A Simple Example + +```python +import click + +@click.command() +@click.option("--count", default=1, help="Number of greetings.") +@click.option("--name", prompt="Your name", help="The person to greet.") +def hello(count, name): + """Simple program that greets NAME for a total of COUNT times.""" + for _ in range(count): + click.echo(f"Hello, {name}!") + +if __name__ == '__main__': + hello() +``` + +``` +$ python hello.py --count=3 +Your name: Click +Hello, Click! +Hello, Click! +Hello, Click! +``` + + +## Donate + +The Pallets organization develops and supports Click and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today][]. + +[please donate today]: https://palletsprojects.com/donate + +## Contributing + +See our [detailed contributing documentation][contrib] for many ways to +contribute, including reporting issues, requesting features, asking or answering +questions, and making PRs. + +[contrib]: https://palletsprojects.com/contributing/ + diff --git a/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/RECORD b/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/RECORD new file mode 100644 index 0000000..9a1cb36 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/RECORD @@ -0,0 +1,40 @@ +click-8.3.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +click-8.3.0.dist-info/METADATA,sha256=P6vpEHZ_MLBt4SO2eB-QaadcOdiznkzaZtJImRo7_V4,2621 +click-8.3.0.dist-info/RECORD,, +click-8.3.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +click-8.3.0.dist-info/licenses/LICENSE.txt,sha256=morRBqOU6FO_4h9C9OctWSgZoigF2ZG18ydQKSkrZY0,1475 +click/__init__.py,sha256=6YyS1aeyknZ0LYweWozNZy0A9nZ_11wmYIhv3cbQrYo,4473 +click/__pycache__/__init__.cpython-311.pyc,, +click/__pycache__/_compat.cpython-311.pyc,, +click/__pycache__/_termui_impl.cpython-311.pyc,, +click/__pycache__/_textwrap.cpython-311.pyc,, +click/__pycache__/_utils.cpython-311.pyc,, +click/__pycache__/_winconsole.cpython-311.pyc,, +click/__pycache__/core.cpython-311.pyc,, +click/__pycache__/decorators.cpython-311.pyc,, +click/__pycache__/exceptions.cpython-311.pyc,, +click/__pycache__/formatting.cpython-311.pyc,, +click/__pycache__/globals.cpython-311.pyc,, +click/__pycache__/parser.cpython-311.pyc,, +click/__pycache__/shell_completion.cpython-311.pyc,, +click/__pycache__/termui.cpython-311.pyc,, +click/__pycache__/testing.cpython-311.pyc,, +click/__pycache__/types.cpython-311.pyc,, +click/__pycache__/utils.cpython-311.pyc,, +click/_compat.py,sha256=v3xBZkFbvA1BXPRkFfBJc6-pIwPI7345m-kQEnpVAs4,18693 +click/_termui_impl.py,sha256=ktpAHyJtNkhyR-x64CQFD6xJQI11fTA3qg2AV3iCToU,26799 +click/_textwrap.py,sha256=BOae0RQ6vg3FkNgSJyOoGzG1meGMxJ_ukWVZKx_v-0o,1400 +click/_utils.py,sha256=kZwtTf5gMuCilJJceS2iTCvRvCY-0aN5rJq8gKw7p8g,943 +click/_winconsole.py,sha256=_vxUuUaxwBhoR0vUWCNuHY8VUefiMdCIyU2SXPqoF-A,8465 +click/core.py,sha256=1A5T8UoAXklIGPTJ83_DJbVi35ehtJS2FTkP_wQ7es0,128855 +click/decorators.py,sha256=5P7abhJtAQYp_KHgjUvhMv464ERwOzrv2enNknlwHyQ,18461 +click/exceptions.py,sha256=8utf8w6V5hJXMnO_ic1FNrtbwuEn1NUu1aDwV8UqnG4,9954 +click/formatting.py,sha256=RVfwwr0rwWNpgGr8NaHodPzkIr7_tUyVh_nDdanLMNc,9730 +click/globals.py,sha256=gM-Nh6A4M0HB_SgkaF5M4ncGGMDHc_flHXu9_oh4GEU,1923 +click/parser.py,sha256=Q31pH0FlQZEq-UXE_ABRzlygEfvxPTuZbWNh4xfXmzw,19010 +click/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +click/shell_completion.py,sha256=Cc4GQUFuWpfQBa9sF5qXeeYI7n3tI_1k6ZdSn4BZbT0,20994 +click/termui.py,sha256=vAYrKC2a7f_NfEIhAThEVYfa__ib5XQbTSCGtJlABRA,30847 +click/testing.py,sha256=EERbzcl1br0mW0qBS9EqkknfNfXB9WQEW0ELIpkvuSs,19102 +click/types.py,sha256=ek54BNSFwPKsqtfT7jsqcc4WHui8AIFVMKM4oVZIXhc,39927 +click/utils.py,sha256=gCUoewdAhA-QLBUUHxrLh4uj6m7T1WjZZMNPvR0I7YA,20257 diff --git a/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/WHEEL b/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.12.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/licenses/LICENSE.txt b/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..d12a849 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click-8.3.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2014 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/netdeploy/lib/python3.11/site-packages/click/__init__.py b/netdeploy/lib/python3.11/site-packages/click/__init__.py new file mode 100644 index 0000000..1aa547c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/__init__.py @@ -0,0 +1,123 @@ +""" +Click is a simple Python module inspired by the stdlib optparse to make +writing command line scripts fun. Unlike other modules, it's based +around a simple API that does not come with too much magic and is +composable. +""" + +from __future__ import annotations + +from .core import Argument as Argument +from .core import Command as Command +from .core import CommandCollection as CommandCollection +from .core import Context as Context +from .core import Group as Group +from .core import Option as Option +from .core import Parameter as Parameter +from .decorators import argument as argument +from .decorators import command as command +from .decorators import confirmation_option as confirmation_option +from .decorators import group as group +from .decorators import help_option as help_option +from .decorators import make_pass_decorator as make_pass_decorator +from .decorators import option as option +from .decorators import pass_context as pass_context +from .decorators import pass_obj as pass_obj +from .decorators import password_option as password_option +from .decorators import version_option as version_option +from .exceptions import Abort as Abort +from .exceptions import BadArgumentUsage as BadArgumentUsage +from .exceptions import BadOptionUsage as BadOptionUsage +from .exceptions import BadParameter as BadParameter +from .exceptions import ClickException as ClickException +from .exceptions import FileError as FileError +from .exceptions import MissingParameter as MissingParameter +from .exceptions import NoSuchOption as NoSuchOption +from .exceptions import UsageError as UsageError +from .formatting import HelpFormatter as HelpFormatter +from .formatting import wrap_text as wrap_text +from .globals import get_current_context as get_current_context +from .termui import clear as clear +from .termui import confirm as confirm +from .termui import echo_via_pager as echo_via_pager +from .termui import edit as edit +from .termui import getchar as getchar +from .termui import launch as launch +from .termui import pause as pause +from .termui import progressbar as progressbar +from .termui import prompt as prompt +from .termui import secho as secho +from .termui import style as style +from .termui import unstyle as unstyle +from .types import BOOL as BOOL +from .types import Choice as Choice +from .types import DateTime as DateTime +from .types import File as File +from .types import FLOAT as FLOAT +from .types import FloatRange as FloatRange +from .types import INT as INT +from .types import IntRange as IntRange +from .types import ParamType as ParamType +from .types import Path as Path +from .types import STRING as STRING +from .types import Tuple as Tuple +from .types import UNPROCESSED as UNPROCESSED +from .types import UUID as UUID +from .utils import echo as echo +from .utils import format_filename as format_filename +from .utils import get_app_dir as get_app_dir +from .utils import get_binary_stream as get_binary_stream +from .utils import get_text_stream as get_text_stream +from .utils import open_file as open_file + + +def __getattr__(name: str) -> object: + import warnings + + if name == "BaseCommand": + from .core import _BaseCommand + + warnings.warn( + "'BaseCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Command' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _BaseCommand + + if name == "MultiCommand": + from .core import _MultiCommand + + warnings.warn( + "'MultiCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Group' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _MultiCommand + + if name == "OptionParser": + from .parser import _OptionParser + + warnings.warn( + "'OptionParser' is deprecated and will be removed in Click 9.0. The" + " old parser is available in 'optparse'.", + DeprecationWarning, + stacklevel=2, + ) + return _OptionParser + + if name == "__version__": + import importlib.metadata + import warnings + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " Click 9.1. Use feature detection or" + " 'importlib.metadata.version(\"click\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("click") + + raise AttributeError(name) diff --git a/netdeploy/lib/python3.11/site-packages/click/_compat.py b/netdeploy/lib/python3.11/site-packages/click/_compat.py new file mode 100644 index 0000000..f2726b9 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/_compat.py @@ -0,0 +1,622 @@ +from __future__ import annotations + +import codecs +import collections.abc as cabc +import io +import os +import re +import sys +import typing as t +from types import TracebackType +from weakref import WeakKeyDictionary + +CYGWIN = sys.platform.startswith("cygwin") +WIN = sys.platform.startswith("win") +auto_wrap_for_ansi: t.Callable[[t.TextIO], t.TextIO] | None = None +_ansi_re = re.compile(r"\033\[[;?0-9]*[a-zA-Z]") + + +def _make_text_stream( + stream: t.BinaryIO, + encoding: str | None, + errors: str | None, + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if encoding is None: + encoding = get_best_encoding(stream) + if errors is None: + errors = "replace" + return _NonClosingTextIOWrapper( + stream, + encoding, + errors, + line_buffering=True, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def is_ascii_encoding(encoding: str) -> bool: + """Checks if a given encoding is ascii.""" + try: + return codecs.lookup(encoding).name == "ascii" + except LookupError: + return False + + +def get_best_encoding(stream: t.IO[t.Any]) -> str: + """Returns the default stream encoding if not found.""" + rv = getattr(stream, "encoding", None) or sys.getdefaultencoding() + if is_ascii_encoding(rv): + return "utf-8" + return rv + + +class _NonClosingTextIOWrapper(io.TextIOWrapper): + def __init__( + self, + stream: t.BinaryIO, + encoding: str | None, + errors: str | None, + force_readable: bool = False, + force_writable: bool = False, + **extra: t.Any, + ) -> None: + self._stream = stream = t.cast( + t.BinaryIO, _FixupStream(stream, force_readable, force_writable) + ) + super().__init__(stream, encoding, errors, **extra) + + def __del__(self) -> None: + try: + self.detach() + except Exception: + pass + + def isatty(self) -> bool: + # https://bitbucket.org/pypy/pypy/issue/1803 + return self._stream.isatty() + + +class _FixupStream: + """The new io interface needs more from streams than streams + traditionally implement. As such, this fix-up code is necessary in + some circumstances. + + The forcing of readable and writable flags are there because some tools + put badly patched objects on sys (one such offender are certain version + of jupyter notebook). + """ + + def __init__( + self, + stream: t.BinaryIO, + force_readable: bool = False, + force_writable: bool = False, + ): + self._stream = stream + self._force_readable = force_readable + self._force_writable = force_writable + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._stream, name) + + def read1(self, size: int) -> bytes: + f = getattr(self._stream, "read1", None) + + if f is not None: + return t.cast(bytes, f(size)) + + return self._stream.read(size) + + def readable(self) -> bool: + if self._force_readable: + return True + x = getattr(self._stream, "readable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.read(0) + except Exception: + return False + return True + + def writable(self) -> bool: + if self._force_writable: + return True + x = getattr(self._stream, "writable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.write(b"") + except Exception: + try: + self._stream.write(b"") + except Exception: + return False + return True + + def seekable(self) -> bool: + x = getattr(self._stream, "seekable", None) + if x is not None: + return t.cast(bool, x()) + try: + self._stream.seek(self._stream.tell()) + except Exception: + return False + return True + + +def _is_binary_reader(stream: t.IO[t.Any], default: bool = False) -> bool: + try: + return isinstance(stream.read(0), bytes) + except Exception: + return default + # This happens in some cases where the stream was already + # closed. In this case, we assume the default. + + +def _is_binary_writer(stream: t.IO[t.Any], default: bool = False) -> bool: + try: + stream.write(b"") + except Exception: + try: + stream.write("") + return False + except Exception: + pass + return default + return True + + +def _find_binary_reader(stream: t.IO[t.Any]) -> t.BinaryIO | None: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_reader(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_reader(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _find_binary_writer(stream: t.IO[t.Any]) -> t.BinaryIO | None: + # We need to figure out if the given stream is already binary. + # This can happen because the official docs recommend detaching + # the streams to get binary streams. Some code might do this, so + # we need to deal with this case explicitly. + if _is_binary_writer(stream, False): + return t.cast(t.BinaryIO, stream) + + buf = getattr(stream, "buffer", None) + + # Same situation here; this time we assume that the buffer is + # actually binary in case it's closed. + if buf is not None and _is_binary_writer(buf, True): + return t.cast(t.BinaryIO, buf) + + return None + + +def _stream_is_misconfigured(stream: t.TextIO) -> bool: + """A stream is misconfigured if its encoding is ASCII.""" + # If the stream does not have an encoding set, we assume it's set + # to ASCII. This appears to happen in certain unittest + # environments. It's not quite clear what the correct behavior is + # but this at least will force Click to recover somehow. + return is_ascii_encoding(getattr(stream, "encoding", None) or "ascii") + + +def _is_compat_stream_attr(stream: t.TextIO, attr: str, value: str | None) -> bool: + """A stream attribute is compatible if it is equal to the + desired value or the desired value is unset and the attribute + has a value. + """ + stream_value = getattr(stream, attr, None) + return stream_value == value or (value is None and stream_value is not None) + + +def _is_compatible_text_stream( + stream: t.TextIO, encoding: str | None, errors: str | None +) -> bool: + """Check if a stream's encoding and errors attributes are + compatible with the desired values. + """ + return _is_compat_stream_attr( + stream, "encoding", encoding + ) and _is_compat_stream_attr(stream, "errors", errors) + + +def _force_correct_text_stream( + text_stream: t.IO[t.Any], + encoding: str | None, + errors: str | None, + is_binary: t.Callable[[t.IO[t.Any], bool], bool], + find_binary: t.Callable[[t.IO[t.Any]], t.BinaryIO | None], + force_readable: bool = False, + force_writable: bool = False, +) -> t.TextIO: + if is_binary(text_stream, False): + binary_reader = t.cast(t.BinaryIO, text_stream) + else: + text_stream = t.cast(t.TextIO, text_stream) + # If the stream looks compatible, and won't default to a + # misconfigured ascii encoding, return it as-is. + if _is_compatible_text_stream(text_stream, encoding, errors) and not ( + encoding is None and _stream_is_misconfigured(text_stream) + ): + return text_stream + + # Otherwise, get the underlying binary reader. + possible_binary_reader = find_binary(text_stream) + + # If that's not possible, silently use the original reader + # and get mojibake instead of exceptions. + if possible_binary_reader is None: + return text_stream + + binary_reader = possible_binary_reader + + # Default errors to replace instead of strict in order to get + # something that works. + if errors is None: + errors = "replace" + + # Wrap the binary stream in a text stream with the correct + # encoding parameters. + return _make_text_stream( + binary_reader, + encoding, + errors, + force_readable=force_readable, + force_writable=force_writable, + ) + + +def _force_correct_text_reader( + text_reader: t.IO[t.Any], + encoding: str | None, + errors: str | None, + force_readable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_reader, + encoding, + errors, + _is_binary_reader, + _find_binary_reader, + force_readable=force_readable, + ) + + +def _force_correct_text_writer( + text_writer: t.IO[t.Any], + encoding: str | None, + errors: str | None, + force_writable: bool = False, +) -> t.TextIO: + return _force_correct_text_stream( + text_writer, + encoding, + errors, + _is_binary_writer, + _find_binary_writer, + force_writable=force_writable, + ) + + +def get_binary_stdin() -> t.BinaryIO: + reader = _find_binary_reader(sys.stdin) + if reader is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdin.") + return reader + + +def get_binary_stdout() -> t.BinaryIO: + writer = _find_binary_writer(sys.stdout) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stdout.") + return writer + + +def get_binary_stderr() -> t.BinaryIO: + writer = _find_binary_writer(sys.stderr) + if writer is None: + raise RuntimeError("Was not able to determine binary stream for sys.stderr.") + return writer + + +def get_text_stdin(encoding: str | None = None, errors: str | None = None) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdin, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_reader(sys.stdin, encoding, errors, force_readable=True) + + +def get_text_stdout(encoding: str | None = None, errors: str | None = None) -> t.TextIO: + rv = _get_windows_console_stream(sys.stdout, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stdout, encoding, errors, force_writable=True) + + +def get_text_stderr(encoding: str | None = None, errors: str | None = None) -> t.TextIO: + rv = _get_windows_console_stream(sys.stderr, encoding, errors) + if rv is not None: + return rv + return _force_correct_text_writer(sys.stderr, encoding, errors, force_writable=True) + + +def _wrap_io_open( + file: str | os.PathLike[str] | int, + mode: str, + encoding: str | None, + errors: str | None, +) -> t.IO[t.Any]: + """Handles not passing ``encoding`` and ``errors`` in binary mode.""" + if "b" in mode: + return open(file, mode) + + return open(file, mode, encoding=encoding, errors=errors) + + +def open_stream( + filename: str | os.PathLike[str], + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, +) -> tuple[t.IO[t.Any], bool]: + binary = "b" in mode + filename = os.fspath(filename) + + # Standard streams first. These are simple because they ignore the + # atomic flag. Use fsdecode to handle Path("-"). + if os.fsdecode(filename) == "-": + if any(m in mode for m in ["w", "a", "x"]): + if binary: + return get_binary_stdout(), False + return get_text_stdout(encoding=encoding, errors=errors), False + if binary: + return get_binary_stdin(), False + return get_text_stdin(encoding=encoding, errors=errors), False + + # Non-atomic writes directly go out through the regular open functions. + if not atomic: + return _wrap_io_open(filename, mode, encoding, errors), True + + # Some usability stuff for atomic writes + if "a" in mode: + raise ValueError( + "Appending to an existing file is not supported, because that" + " would involve an expensive `copy`-operation to a temporary" + " file. Open the file in normal `w`-mode and copy explicitly" + " if that's what you're after." + ) + if "x" in mode: + raise ValueError("Use the `overwrite`-parameter instead.") + if "w" not in mode: + raise ValueError("Atomic writes only make sense with `w`-mode.") + + # Atomic writes are more complicated. They work by opening a file + # as a proxy in the same folder and then using the fdopen + # functionality to wrap it in a Python file. Then we wrap it in an + # atomic file that moves the file over on close. + import errno + import random + + try: + perm: int | None = os.stat(filename).st_mode + except OSError: + perm = None + + flags = os.O_RDWR | os.O_CREAT | os.O_EXCL + + if binary: + flags |= getattr(os, "O_BINARY", 0) + + while True: + tmp_filename = os.path.join( + os.path.dirname(filename), + f".__atomic-write{random.randrange(1 << 32):08x}", + ) + try: + fd = os.open(tmp_filename, flags, 0o666 if perm is None else perm) + break + except OSError as e: + if e.errno == errno.EEXIST or ( + os.name == "nt" + and e.errno == errno.EACCES + and os.path.isdir(e.filename) + and os.access(e.filename, os.W_OK) + ): + continue + raise + + if perm is not None: + os.chmod(tmp_filename, perm) # in case perm includes bits in umask + + f = _wrap_io_open(fd, mode, encoding, errors) + af = _AtomicFile(f, tmp_filename, os.path.realpath(filename)) + return t.cast(t.IO[t.Any], af), True + + +class _AtomicFile: + def __init__(self, f: t.IO[t.Any], tmp_filename: str, real_filename: str) -> None: + self._f = f + self._tmp_filename = tmp_filename + self._real_filename = real_filename + self.closed = False + + @property + def name(self) -> str: + return self._real_filename + + def close(self, delete: bool = False) -> None: + if self.closed: + return + self._f.close() + os.replace(self._tmp_filename, self._real_filename) + self.closed = True + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._f, name) + + def __enter__(self) -> _AtomicFile: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.close(delete=exc_type is not None) + + def __repr__(self) -> str: + return repr(self._f) + + +def strip_ansi(value: str) -> str: + return _ansi_re.sub("", value) + + +def _is_jupyter_kernel_output(stream: t.IO[t.Any]) -> bool: + while isinstance(stream, (_FixupStream, _NonClosingTextIOWrapper)): + stream = stream._stream + + return stream.__class__.__module__.startswith("ipykernel.") + + +def should_strip_ansi( + stream: t.IO[t.Any] | None = None, color: bool | None = None +) -> bool: + if color is None: + if stream is None: + stream = sys.stdin + return not isatty(stream) and not _is_jupyter_kernel_output(stream) + return not color + + +# On Windows, wrap the output streams with colorama to support ANSI +# color codes. +# NOTE: double check is needed so mypy does not analyze this on Linux +if sys.platform.startswith("win") and WIN: + from ._winconsole import _get_windows_console_stream + + def _get_argv_encoding() -> str: + import locale + + return locale.getpreferredencoding() + + _ansi_stream_wrappers: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def auto_wrap_for_ansi(stream: t.TextIO, color: bool | None = None) -> t.TextIO: + """Support ANSI color and style codes on Windows by wrapping a + stream with colorama. + """ + try: + cached = _ansi_stream_wrappers.get(stream) + except Exception: + cached = None + + if cached is not None: + return cached + + import colorama + + strip = should_strip_ansi(stream, color) + ansi_wrapper = colorama.AnsiToWin32(stream, strip=strip) + rv = t.cast(t.TextIO, ansi_wrapper.stream) + _write = rv.write + + def _safe_write(s: str) -> int: + try: + return _write(s) + except BaseException: + ansi_wrapper.reset_all() + raise + + rv.write = _safe_write # type: ignore[method-assign] + + try: + _ansi_stream_wrappers[stream] = rv + except Exception: + pass + + return rv + +else: + + def _get_argv_encoding() -> str: + return getattr(sys.stdin, "encoding", None) or sys.getfilesystemencoding() + + def _get_windows_console_stream( + f: t.TextIO, encoding: str | None, errors: str | None + ) -> t.TextIO | None: + return None + + +def term_len(x: str) -> int: + return len(strip_ansi(x)) + + +def isatty(stream: t.IO[t.Any]) -> bool: + try: + return stream.isatty() + except Exception: + return False + + +def _make_cached_stream_func( + src_func: t.Callable[[], t.TextIO | None], + wrapper_func: t.Callable[[], t.TextIO], +) -> t.Callable[[], t.TextIO | None]: + cache: cabc.MutableMapping[t.TextIO, t.TextIO] = WeakKeyDictionary() + + def func() -> t.TextIO | None: + stream = src_func() + + if stream is None: + return None + + try: + rv = cache.get(stream) + except Exception: + rv = None + if rv is not None: + return rv + rv = wrapper_func() + try: + cache[stream] = rv + except Exception: + pass + return rv + + return func + + +_default_text_stdin = _make_cached_stream_func(lambda: sys.stdin, get_text_stdin) +_default_text_stdout = _make_cached_stream_func(lambda: sys.stdout, get_text_stdout) +_default_text_stderr = _make_cached_stream_func(lambda: sys.stderr, get_text_stderr) + + +binary_streams: cabc.Mapping[str, t.Callable[[], t.BinaryIO]] = { + "stdin": get_binary_stdin, + "stdout": get_binary_stdout, + "stderr": get_binary_stderr, +} + +text_streams: cabc.Mapping[str, t.Callable[[str | None, str | None], t.TextIO]] = { + "stdin": get_text_stdin, + "stdout": get_text_stdout, + "stderr": get_text_stderr, +} diff --git a/netdeploy/lib/python3.11/site-packages/click/_termui_impl.py b/netdeploy/lib/python3.11/site-packages/click/_termui_impl.py new file mode 100644 index 0000000..47f87b8 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/_termui_impl.py @@ -0,0 +1,847 @@ +""" +This module contains implementations for the termui module. To keep the +import time of Click down, some infrequently used functionality is +placed in this module and only imported as needed. +""" + +from __future__ import annotations + +import collections.abc as cabc +import contextlib +import math +import os +import shlex +import sys +import time +import typing as t +from gettext import gettext as _ +from io import StringIO +from pathlib import Path +from types import TracebackType + +from ._compat import _default_text_stdout +from ._compat import CYGWIN +from ._compat import get_best_encoding +from ._compat import isatty +from ._compat import open_stream +from ._compat import strip_ansi +from ._compat import term_len +from ._compat import WIN +from .exceptions import ClickException +from .utils import echo + +V = t.TypeVar("V") + +if os.name == "nt": + BEFORE_BAR = "\r" + AFTER_BAR = "\n" +else: + BEFORE_BAR = "\r\033[?25l" + AFTER_BAR = "\033[?25h\n" + + +class ProgressBar(t.Generic[V]): + def __init__( + self, + iterable: cabc.Iterable[V] | None, + length: int | None = None, + fill_char: str = "#", + empty_char: str = " ", + bar_template: str = "%(bar)s", + info_sep: str = " ", + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + item_show_func: t.Callable[[V | None], str | None] | None = None, + label: str | None = None, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, + width: int = 30, + ) -> None: + self.fill_char = fill_char + self.empty_char = empty_char + self.bar_template = bar_template + self.info_sep = info_sep + self.hidden = hidden + self.show_eta = show_eta + self.show_percent = show_percent + self.show_pos = show_pos + self.item_show_func = item_show_func + self.label: str = label or "" + + if file is None: + file = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if file is None: + file = StringIO() + + self.file = file + self.color = color + self.update_min_steps = update_min_steps + self._completed_intervals = 0 + self.width: int = width + self.autowidth: bool = width == 0 + + if length is None: + from operator import length_hint + + length = length_hint(iterable, -1) + + if length == -1: + length = None + if iterable is None: + if length is None: + raise TypeError("iterable or length is required") + iterable = t.cast("cabc.Iterable[V]", range(length)) + self.iter: cabc.Iterable[V] = iter(iterable) + self.length = length + self.pos: int = 0 + self.avg: list[float] = [] + self.last_eta: float + self.start: float + self.start = self.last_eta = time.time() + self.eta_known: bool = False + self.finished: bool = False + self.max_width: int | None = None + self.entered: bool = False + self.current_item: V | None = None + self._is_atty = isatty(self.file) + self._last_line: str | None = None + + def __enter__(self) -> ProgressBar[V]: + self.entered = True + self.render_progress() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.render_finish() + + def __iter__(self) -> cabc.Iterator[V]: + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + self.render_progress() + return self.generator() + + def __next__(self) -> V: + # Iteration is defined in terms of a generator function, + # returned by iter(self); use that to define next(). This works + # because `self.iter` is an iterable consumed by that generator, + # so it is re-entry safe. Calling `next(self.generator())` + # twice works and does "what you want". + return next(iter(self)) + + def render_finish(self) -> None: + if self.hidden or not self._is_atty: + return + self.file.write(AFTER_BAR) + self.file.flush() + + @property + def pct(self) -> float: + if self.finished: + return 1.0 + return min(self.pos / (float(self.length or 1) or 1), 1.0) + + @property + def time_per_iteration(self) -> float: + if not self.avg: + return 0.0 + return sum(self.avg) / float(len(self.avg)) + + @property + def eta(self) -> float: + if self.length is not None and not self.finished: + return self.time_per_iteration * (self.length - self.pos) + return 0.0 + + def format_eta(self) -> str: + if self.eta_known: + t = int(self.eta) + seconds = t % 60 + t //= 60 + minutes = t % 60 + t //= 60 + hours = t % 24 + t //= 24 + if t > 0: + return f"{t}d {hours:02}:{minutes:02}:{seconds:02}" + else: + return f"{hours:02}:{minutes:02}:{seconds:02}" + return "" + + def format_pos(self) -> str: + pos = str(self.pos) + if self.length is not None: + pos += f"/{self.length}" + return pos + + def format_pct(self) -> str: + return f"{int(self.pct * 100): 4}%"[1:] + + def format_bar(self) -> str: + if self.length is not None: + bar_length = int(self.pct * self.width) + bar = self.fill_char * bar_length + bar += self.empty_char * (self.width - bar_length) + elif self.finished: + bar = self.fill_char * self.width + else: + chars = list(self.empty_char * (self.width or 1)) + if self.time_per_iteration != 0: + chars[ + int( + (math.cos(self.pos * self.time_per_iteration) / 2.0 + 0.5) + * self.width + ) + ] = self.fill_char + bar = "".join(chars) + return bar + + def format_progress_line(self) -> str: + show_percent = self.show_percent + + info_bits = [] + if self.length is not None and show_percent is None: + show_percent = not self.show_pos + + if self.show_pos: + info_bits.append(self.format_pos()) + if show_percent: + info_bits.append(self.format_pct()) + if self.show_eta and self.eta_known and not self.finished: + info_bits.append(self.format_eta()) + if self.item_show_func is not None: + item_info = self.item_show_func(self.current_item) + if item_info is not None: + info_bits.append(item_info) + + return ( + self.bar_template + % { + "label": self.label, + "bar": self.format_bar(), + "info": self.info_sep.join(info_bits), + } + ).rstrip() + + def render_progress(self) -> None: + if self.hidden: + return + + if not self._is_atty: + # Only output the label once if the output is not a TTY. + if self._last_line != self.label: + self._last_line = self.label + echo(self.label, file=self.file, color=self.color) + return + + buf = [] + # Update width in case the terminal has been resized + if self.autowidth: + import shutil + + old_width = self.width + self.width = 0 + clutter_length = term_len(self.format_progress_line()) + new_width = max(0, shutil.get_terminal_size().columns - clutter_length) + if new_width < old_width and self.max_width is not None: + buf.append(BEFORE_BAR) + buf.append(" " * self.max_width) + self.max_width = new_width + self.width = new_width + + clear_width = self.width + if self.max_width is not None: + clear_width = self.max_width + + buf.append(BEFORE_BAR) + line = self.format_progress_line() + line_len = term_len(line) + if self.max_width is None or self.max_width < line_len: + self.max_width = line_len + + buf.append(line) + buf.append(" " * (clear_width - line_len)) + line = "".join(buf) + # Render the line only if it changed. + + if line != self._last_line: + self._last_line = line + echo(line, file=self.file, color=self.color, nl=False) + self.file.flush() + + def make_step(self, n_steps: int) -> None: + self.pos += n_steps + if self.length is not None and self.pos >= self.length: + self.finished = True + + if (time.time() - self.last_eta) < 1.0: + return + + self.last_eta = time.time() + + # self.avg is a rolling list of length <= 7 of steps where steps are + # defined as time elapsed divided by the total progress through + # self.length. + if self.pos: + step = (time.time() - self.start) / self.pos + else: + step = time.time() - self.start + + self.avg = self.avg[-6:] + [step] + + self.eta_known = self.length is not None + + def update(self, n_steps: int, current_item: V | None = None) -> None: + """Update the progress bar by advancing a specified number of + steps, and optionally set the ``current_item`` for this new + position. + + :param n_steps: Number of steps to advance. + :param current_item: Optional item to set as ``current_item`` + for the updated position. + + .. versionchanged:: 8.0 + Added the ``current_item`` optional parameter. + + .. versionchanged:: 8.0 + Only render when the number of steps meets the + ``update_min_steps`` threshold. + """ + if current_item is not None: + self.current_item = current_item + + self._completed_intervals += n_steps + + if self._completed_intervals >= self.update_min_steps: + self.make_step(self._completed_intervals) + self.render_progress() + self._completed_intervals = 0 + + def finish(self) -> None: + self.eta_known = False + self.current_item = None + self.finished = True + + def generator(self) -> cabc.Iterator[V]: + """Return a generator which yields the items added to the bar + during construction, and updates the progress bar *after* the + yielded block returns. + """ + # WARNING: the iterator interface for `ProgressBar` relies on + # this and only works because this is a simple generator which + # doesn't create or manage additional state. If this function + # changes, the impact should be evaluated both against + # `iter(bar)` and `next(bar)`. `next()` in particular may call + # `self.generator()` repeatedly, and this must remain safe in + # order for that interface to work. + if not self.entered: + raise RuntimeError("You need to use progress bars in a with block.") + + if not self._is_atty: + yield from self.iter + else: + for rv in self.iter: + self.current_item = rv + + # This allows show_item_func to be updated before the + # item is processed. Only trigger at the beginning of + # the update interval. + if self._completed_intervals == 0: + self.render_progress() + + yield rv + self.update(1) + + self.finish() + self.render_progress() + + +def pager(generator: cabc.Iterable[str], color: bool | None = None) -> None: + """Decide what method to use for paging through text.""" + stdout = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if stdout is None: + stdout = StringIO() + + if not isatty(sys.stdin) or not isatty(stdout): + return _nullpager(stdout, generator, color) + + # Split and normalize the pager command into parts. + pager_cmd_parts = shlex.split(os.environ.get("PAGER", ""), posix=False) + if pager_cmd_parts: + if WIN: + if _tempfilepager(generator, pager_cmd_parts, color): + return + elif _pipepager(generator, pager_cmd_parts, color): + return + + if os.environ.get("TERM") in ("dumb", "emacs"): + return _nullpager(stdout, generator, color) + if (WIN or sys.platform.startswith("os2")) and _tempfilepager( + generator, ["more"], color + ): + return + if _pipepager(generator, ["less"], color): + return + + import tempfile + + fd, filename = tempfile.mkstemp() + os.close(fd) + try: + if _pipepager(generator, ["more"], color): + return + return _nullpager(stdout, generator, color) + finally: + os.unlink(filename) + + +def _pipepager( + generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None +) -> bool: + """Page through text by feeding it to another program. Invoking a + pager through this might support colors. + + Returns `True` if the command was found, `False` otherwise and thus another + pager should be attempted. + """ + # Split the command into the invoked CLI and its parameters. + if not cmd_parts: + return False + + import shutil + + cmd = cmd_parts[0] + cmd_params = cmd_parts[1:] + + cmd_filepath = shutil.which(cmd) + if not cmd_filepath: + return False + # Resolves symlinks and produces a normalized absolute path string. + cmd_path = Path(cmd_filepath).resolve() + cmd_name = cmd_path.name + + import subprocess + + # Make a local copy of the environment to not affect the global one. + env = dict(os.environ) + + # If we're piping to less and the user hasn't decided on colors, we enable + # them by default we find the -R flag in the command line arguments. + if color is None and cmd_name == "less": + less_flags = f"{os.environ.get('LESS', '')}{' '.join(cmd_params)}" + if not less_flags: + env["LESS"] = "-R" + color = True + elif "r" in less_flags or "R" in less_flags: + color = True + + c = subprocess.Popen( + [str(cmd_path)] + cmd_params, + shell=True, + stdin=subprocess.PIPE, + env=env, + errors="replace", + text=True, + ) + assert c.stdin is not None + try: + for text in generator: + if not color: + text = strip_ansi(text) + + c.stdin.write(text) + except BrokenPipeError: + # In case the pager exited unexpectedly, ignore the broken pipe error. + pass + except Exception as e: + # In case there is an exception we want to close the pager immediately + # and let the caller handle it. + # Otherwise the pager will keep running, and the user may not notice + # the error message, or worse yet it may leave the terminal in a broken state. + c.terminate() + raise e + finally: + # We must close stdin and wait for the pager to exit before we continue + try: + c.stdin.close() + # Close implies flush, so it might throw a BrokenPipeError if the pager + # process exited already. + except BrokenPipeError: + pass + + # Less doesn't respect ^C, but catches it for its own UI purposes (aborting + # search or other commands inside less). + # + # That means when the user hits ^C, the parent process (click) terminates, + # but less is still alive, paging the output and messing up the terminal. + # + # If the user wants to make the pager exit on ^C, they should set + # `LESS='-K'`. It's not our decision to make. + while True: + try: + c.wait() + except KeyboardInterrupt: + pass + else: + break + + return True + + +def _tempfilepager( + generator: cabc.Iterable[str], cmd_parts: list[str], color: bool | None +) -> bool: + """Page through text by invoking a program on a temporary file. + + Returns `True` if the command was found, `False` otherwise and thus another + pager should be attempted. + """ + # Split the command into the invoked CLI and its parameters. + if not cmd_parts: + return False + + import shutil + + cmd = cmd_parts[0] + + cmd_filepath = shutil.which(cmd) + if not cmd_filepath: + return False + # Resolves symlinks and produces a normalized absolute path string. + cmd_path = Path(cmd_filepath).resolve() + + import subprocess + import tempfile + + fd, filename = tempfile.mkstemp() + # TODO: This never terminates if the passed generator never terminates. + text = "".join(generator) + if not color: + text = strip_ansi(text) + encoding = get_best_encoding(sys.stdout) + with open_stream(filename, "wb")[0] as f: + f.write(text.encode(encoding)) + try: + subprocess.call([str(cmd_path), filename]) + except OSError: + # Command not found + pass + finally: + os.close(fd) + os.unlink(filename) + + return True + + +def _nullpager( + stream: t.TextIO, generator: cabc.Iterable[str], color: bool | None +) -> None: + """Simply print unformatted text. This is the ultimate fallback.""" + for text in generator: + if not color: + text = strip_ansi(text) + stream.write(text) + + +class Editor: + def __init__( + self, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", + ) -> None: + self.editor = editor + self.env = env + self.require_save = require_save + self.extension = extension + + def get_editor(self) -> str: + if self.editor is not None: + return self.editor + for key in "VISUAL", "EDITOR": + rv = os.environ.get(key) + if rv: + return rv + if WIN: + return "notepad" + + from shutil import which + + for editor in "sensible-editor", "vim", "nano": + if which(editor) is not None: + return editor + return "vi" + + def edit_files(self, filenames: cabc.Iterable[str]) -> None: + import subprocess + + editor = self.get_editor() + environ: dict[str, str] | None = None + + if self.env: + environ = os.environ.copy() + environ.update(self.env) + + exc_filename = " ".join(f'"{filename}"' for filename in filenames) + + try: + c = subprocess.Popen( + args=f"{editor} {exc_filename}", env=environ, shell=True + ) + exit_code = c.wait() + if exit_code != 0: + raise ClickException( + _("{editor}: Editing failed").format(editor=editor) + ) + except OSError as e: + raise ClickException( + _("{editor}: Editing failed: {e}").format(editor=editor, e=e) + ) from e + + @t.overload + def edit(self, text: bytes | bytearray) -> bytes | None: ... + + # We cannot know whether or not the type expected is str or bytes when None + # is passed, so str is returned as that was what was done before. + @t.overload + def edit(self, text: str | None) -> str | None: ... + + def edit(self, text: str | bytes | bytearray | None) -> str | bytes | None: + import tempfile + + if text is None: + data: bytes | bytearray = b"" + elif isinstance(text, (bytes, bytearray)): + data = text + else: + if text and not text.endswith("\n"): + text += "\n" + + if WIN: + data = text.replace("\n", "\r\n").encode("utf-8-sig") + else: + data = text.encode("utf-8") + + fd, name = tempfile.mkstemp(prefix="editor-", suffix=self.extension) + f: t.BinaryIO + + try: + with os.fdopen(fd, "wb") as f: + f.write(data) + + # If the filesystem resolution is 1 second, like Mac OS + # 10.12 Extended, or 2 seconds, like FAT32, and the editor + # closes very fast, require_save can fail. Set the modified + # time to be 2 seconds in the past to work around this. + os.utime(name, (os.path.getatime(name), os.path.getmtime(name) - 2)) + # Depending on the resolution, the exact value might not be + # recorded, so get the new recorded value. + timestamp = os.path.getmtime(name) + + self.edit_files((name,)) + + if self.require_save and os.path.getmtime(name) == timestamp: + return None + + with open(name, "rb") as f: + rv = f.read() + + if isinstance(text, (bytes, bytearray)): + return rv + + return rv.decode("utf-8-sig").replace("\r\n", "\n") + finally: + os.unlink(name) + + +def open_url(url: str, wait: bool = False, locate: bool = False) -> int: + import subprocess + + def _unquote_file(url: str) -> str: + from urllib.parse import unquote + + if url.startswith("file://"): + url = unquote(url[7:]) + + return url + + if sys.platform == "darwin": + args = ["open"] + if wait: + args.append("-W") + if locate: + args.append("-R") + args.append(_unquote_file(url)) + null = open("/dev/null", "w") + try: + return subprocess.Popen(args, stderr=null).wait() + finally: + null.close() + elif WIN: + if locate: + url = _unquote_file(url) + args = ["explorer", f"/select,{url}"] + else: + args = ["start"] + if wait: + args.append("/WAIT") + args.append("") + args.append(url) + try: + return subprocess.call(args) + except OSError: + # Command not found + return 127 + elif CYGWIN: + if locate: + url = _unquote_file(url) + args = ["cygstart", os.path.dirname(url)] + else: + args = ["cygstart"] + if wait: + args.append("-w") + args.append(url) + try: + return subprocess.call(args) + except OSError: + # Command not found + return 127 + + try: + if locate: + url = os.path.dirname(_unquote_file(url)) or "." + else: + url = _unquote_file(url) + c = subprocess.Popen(["xdg-open", url]) + if wait: + return c.wait() + return 0 + except OSError: + if url.startswith(("http://", "https://")) and not locate and not wait: + import webbrowser + + webbrowser.open(url) + return 0 + return 1 + + +def _translate_ch_to_exc(ch: str) -> None: + if ch == "\x03": + raise KeyboardInterrupt() + + if ch == "\x04" and not WIN: # Unix-like, Ctrl+D + raise EOFError() + + if ch == "\x1a" and WIN: # Windows, Ctrl+Z + raise EOFError() + + return None + + +if sys.platform == "win32": + import msvcrt + + @contextlib.contextmanager + def raw_terminal() -> cabc.Iterator[int]: + yield -1 + + def getchar(echo: bool) -> str: + # The function `getch` will return a bytes object corresponding to + # the pressed character. Since Windows 10 build 1803, it will also + # return \x00 when called a second time after pressing a regular key. + # + # `getwch` does not share this probably-bugged behavior. Moreover, it + # returns a Unicode object by default, which is what we want. + # + # Either of these functions will return \x00 or \xe0 to indicate + # a special key, and you need to call the same function again to get + # the "rest" of the code. The fun part is that \u00e0 is + # "latin small letter a with grave", so if you type that on a French + # keyboard, you _also_ get a \xe0. + # E.g., consider the Up arrow. This returns \xe0 and then \x48. The + # resulting Unicode string reads as "a with grave" + "capital H". + # This is indistinguishable from when the user actually types + # "a with grave" and then "capital H". + # + # When \xe0 is returned, we assume it's part of a special-key sequence + # and call `getwch` again, but that means that when the user types + # the \u00e0 character, `getchar` doesn't return until a second + # character is typed. + # The alternative is returning immediately, but that would mess up + # cross-platform handling of arrow keys and others that start with + # \xe0. Another option is using `getch`, but then we can't reliably + # read non-ASCII characters, because return values of `getch` are + # limited to the current 8-bit codepage. + # + # Anyway, Click doesn't claim to do this Right(tm), and using `getwch` + # is doing the right thing in more situations than with `getch`. + + if echo: + func = t.cast(t.Callable[[], str], msvcrt.getwche) + else: + func = t.cast(t.Callable[[], str], msvcrt.getwch) + + rv = func() + + if rv in ("\x00", "\xe0"): + # \x00 and \xe0 are control characters that indicate special key, + # see above. + rv += func() + + _translate_ch_to_exc(rv) + return rv + +else: + import termios + import tty + + @contextlib.contextmanager + def raw_terminal() -> cabc.Iterator[int]: + f: t.TextIO | None + fd: int + + if not isatty(sys.stdin): + f = open("/dev/tty") + fd = f.fileno() + else: + fd = sys.stdin.fileno() + f = None + + try: + old_settings = termios.tcgetattr(fd) + + try: + tty.setraw(fd) + yield fd + finally: + termios.tcsetattr(fd, termios.TCSADRAIN, old_settings) + sys.stdout.flush() + + if f is not None: + f.close() + except termios.error: + pass + + def getchar(echo: bool) -> str: + with raw_terminal() as fd: + ch = os.read(fd, 32).decode(get_best_encoding(sys.stdin), "replace") + + if echo and isatty(sys.stdout): + sys.stdout.write(ch) + + _translate_ch_to_exc(ch) + return ch diff --git a/netdeploy/lib/python3.11/site-packages/click/_textwrap.py b/netdeploy/lib/python3.11/site-packages/click/_textwrap.py new file mode 100644 index 0000000..97fbee3 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/_textwrap.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import collections.abc as cabc +import textwrap +from contextlib import contextmanager + + +class TextWrapper(textwrap.TextWrapper): + def _handle_long_word( + self, + reversed_chunks: list[str], + cur_line: list[str], + cur_len: int, + width: int, + ) -> None: + space_left = max(width - cur_len, 1) + + if self.break_long_words: + last = reversed_chunks[-1] + cut = last[:space_left] + res = last[space_left:] + cur_line.append(cut) + reversed_chunks[-1] = res + elif not cur_line: + cur_line.append(reversed_chunks.pop()) + + @contextmanager + def extra_indent(self, indent: str) -> cabc.Iterator[None]: + old_initial_indent = self.initial_indent + old_subsequent_indent = self.subsequent_indent + self.initial_indent += indent + self.subsequent_indent += indent + + try: + yield + finally: + self.initial_indent = old_initial_indent + self.subsequent_indent = old_subsequent_indent + + def indent_only(self, text: str) -> str: + rv = [] + + for idx, line in enumerate(text.splitlines()): + indent = self.initial_indent + + if idx > 0: + indent = self.subsequent_indent + + rv.append(f"{indent}{line}") + + return "\n".join(rv) diff --git a/netdeploy/lib/python3.11/site-packages/click/_utils.py b/netdeploy/lib/python3.11/site-packages/click/_utils.py new file mode 100644 index 0000000..09fb008 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/_utils.py @@ -0,0 +1,36 @@ +from __future__ import annotations + +import enum +import typing as t + + +class Sentinel(enum.Enum): + """Enum used to define sentinel values. + + .. seealso:: + + `PEP 661 - Sentinel Values `_. + """ + + UNSET = object() + FLAG_NEEDS_VALUE = object() + + def __repr__(self) -> str: + return f"{self.__class__.__name__}.{self.name}" + + +UNSET = Sentinel.UNSET +"""Sentinel used to indicate that a value is not set.""" + +FLAG_NEEDS_VALUE = Sentinel.FLAG_NEEDS_VALUE +"""Sentinel used to indicate an option was passed as a flag without a +value but is not a flag option. + +``Option.consume_value`` uses this to prompt or use the ``flag_value``. +""" + +T_UNSET = t.Literal[UNSET] # type: ignore[valid-type] +"""Type hint for the :data:`UNSET` sentinel value.""" + +T_FLAG_NEEDS_VALUE = t.Literal[FLAG_NEEDS_VALUE] # type: ignore[valid-type] +"""Type hint for the :data:`FLAG_NEEDS_VALUE` sentinel value.""" diff --git a/netdeploy/lib/python3.11/site-packages/click/_winconsole.py b/netdeploy/lib/python3.11/site-packages/click/_winconsole.py new file mode 100644 index 0000000..e56c7c6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/_winconsole.py @@ -0,0 +1,296 @@ +# This module is based on the excellent work by Adam Bartoš who +# provided a lot of what went into the implementation here in +# the discussion to issue1602 in the Python bug tracker. +# +# There are some general differences in regards to how this works +# compared to the original patches as we do not need to patch +# the entire interpreter but just work in our little world of +# echo and prompt. +from __future__ import annotations + +import collections.abc as cabc +import io +import sys +import time +import typing as t +from ctypes import Array +from ctypes import byref +from ctypes import c_char +from ctypes import c_char_p +from ctypes import c_int +from ctypes import c_ssize_t +from ctypes import c_ulong +from ctypes import c_void_p +from ctypes import POINTER +from ctypes import py_object +from ctypes import Structure +from ctypes.wintypes import DWORD +from ctypes.wintypes import HANDLE +from ctypes.wintypes import LPCWSTR +from ctypes.wintypes import LPWSTR + +from ._compat import _NonClosingTextIOWrapper + +assert sys.platform == "win32" +import msvcrt # noqa: E402 +from ctypes import windll # noqa: E402 +from ctypes import WINFUNCTYPE # noqa: E402 + +c_ssize_p = POINTER(c_ssize_t) + +kernel32 = windll.kernel32 +GetStdHandle = kernel32.GetStdHandle +ReadConsoleW = kernel32.ReadConsoleW +WriteConsoleW = kernel32.WriteConsoleW +GetConsoleMode = kernel32.GetConsoleMode +GetLastError = kernel32.GetLastError +GetCommandLineW = WINFUNCTYPE(LPWSTR)(("GetCommandLineW", windll.kernel32)) +CommandLineToArgvW = WINFUNCTYPE(POINTER(LPWSTR), LPCWSTR, POINTER(c_int))( + ("CommandLineToArgvW", windll.shell32) +) +LocalFree = WINFUNCTYPE(c_void_p, c_void_p)(("LocalFree", windll.kernel32)) + +STDIN_HANDLE = GetStdHandle(-10) +STDOUT_HANDLE = GetStdHandle(-11) +STDERR_HANDLE = GetStdHandle(-12) + +PyBUF_SIMPLE = 0 +PyBUF_WRITABLE = 1 + +ERROR_SUCCESS = 0 +ERROR_NOT_ENOUGH_MEMORY = 8 +ERROR_OPERATION_ABORTED = 995 + +STDIN_FILENO = 0 +STDOUT_FILENO = 1 +STDERR_FILENO = 2 + +EOF = b"\x1a" +MAX_BYTES_WRITTEN = 32767 + +if t.TYPE_CHECKING: + try: + # Using `typing_extensions.Buffer` instead of `collections.abc` + # on Windows for some reason does not have `Sized` implemented. + from collections.abc import Buffer # type: ignore + except ImportError: + from typing_extensions import Buffer + +try: + from ctypes import pythonapi +except ImportError: + # On PyPy we cannot get buffers so our ability to operate here is + # severely limited. + get_buffer = None +else: + + class Py_buffer(Structure): + _fields_ = [ # noqa: RUF012 + ("buf", c_void_p), + ("obj", py_object), + ("len", c_ssize_t), + ("itemsize", c_ssize_t), + ("readonly", c_int), + ("ndim", c_int), + ("format", c_char_p), + ("shape", c_ssize_p), + ("strides", c_ssize_p), + ("suboffsets", c_ssize_p), + ("internal", c_void_p), + ] + + PyObject_GetBuffer = pythonapi.PyObject_GetBuffer + PyBuffer_Release = pythonapi.PyBuffer_Release + + def get_buffer(obj: Buffer, writable: bool = False) -> Array[c_char]: + buf = Py_buffer() + flags: int = PyBUF_WRITABLE if writable else PyBUF_SIMPLE + PyObject_GetBuffer(py_object(obj), byref(buf), flags) + + try: + buffer_type = c_char * buf.len + out: Array[c_char] = buffer_type.from_address(buf.buf) + return out + finally: + PyBuffer_Release(byref(buf)) + + +class _WindowsConsoleRawIOBase(io.RawIOBase): + def __init__(self, handle: int | None) -> None: + self.handle = handle + + def isatty(self) -> t.Literal[True]: + super().isatty() + return True + + +class _WindowsConsoleReader(_WindowsConsoleRawIOBase): + def readable(self) -> t.Literal[True]: + return True + + def readinto(self, b: Buffer) -> int: + bytes_to_be_read = len(b) + if not bytes_to_be_read: + return 0 + elif bytes_to_be_read % 2: + raise ValueError( + "cannot read odd number of bytes from UTF-16-LE encoded console" + ) + + buffer = get_buffer(b, writable=True) + code_units_to_be_read = bytes_to_be_read // 2 + code_units_read = c_ulong() + + rv = ReadConsoleW( + HANDLE(self.handle), + buffer, + code_units_to_be_read, + byref(code_units_read), + None, + ) + if GetLastError() == ERROR_OPERATION_ABORTED: + # wait for KeyboardInterrupt + time.sleep(0.1) + if not rv: + raise OSError(f"Windows error: {GetLastError()}") + + if buffer[0] == EOF: + return 0 + return 2 * code_units_read.value + + +class _WindowsConsoleWriter(_WindowsConsoleRawIOBase): + def writable(self) -> t.Literal[True]: + return True + + @staticmethod + def _get_error_message(errno: int) -> str: + if errno == ERROR_SUCCESS: + return "ERROR_SUCCESS" + elif errno == ERROR_NOT_ENOUGH_MEMORY: + return "ERROR_NOT_ENOUGH_MEMORY" + return f"Windows error {errno}" + + def write(self, b: Buffer) -> int: + bytes_to_be_written = len(b) + buf = get_buffer(b) + code_units_to_be_written = min(bytes_to_be_written, MAX_BYTES_WRITTEN) // 2 + code_units_written = c_ulong() + + WriteConsoleW( + HANDLE(self.handle), + buf, + code_units_to_be_written, + byref(code_units_written), + None, + ) + bytes_written = 2 * code_units_written.value + + if bytes_written == 0 and bytes_to_be_written > 0: + raise OSError(self._get_error_message(GetLastError())) + return bytes_written + + +class ConsoleStream: + def __init__(self, text_stream: t.TextIO, byte_stream: t.BinaryIO) -> None: + self._text_stream = text_stream + self.buffer = byte_stream + + @property + def name(self) -> str: + return self.buffer.name + + def write(self, x: t.AnyStr) -> int: + if isinstance(x, str): + return self._text_stream.write(x) + try: + self.flush() + except Exception: + pass + return self.buffer.write(x) + + def writelines(self, lines: cabc.Iterable[t.AnyStr]) -> None: + for line in lines: + self.write(line) + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._text_stream, name) + + def isatty(self) -> bool: + return self.buffer.isatty() + + def __repr__(self) -> str: + return f"" + + +def _get_text_stdin(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedReader(_WindowsConsoleReader(STDIN_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +def _get_text_stdout(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDOUT_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +def _get_text_stderr(buffer_stream: t.BinaryIO) -> t.TextIO: + text_stream = _NonClosingTextIOWrapper( + io.BufferedWriter(_WindowsConsoleWriter(STDERR_HANDLE)), + "utf-16-le", + "strict", + line_buffering=True, + ) + return t.cast(t.TextIO, ConsoleStream(text_stream, buffer_stream)) + + +_stream_factories: cabc.Mapping[int, t.Callable[[t.BinaryIO], t.TextIO]] = { + 0: _get_text_stdin, + 1: _get_text_stdout, + 2: _get_text_stderr, +} + + +def _is_console(f: t.TextIO) -> bool: + if not hasattr(f, "fileno"): + return False + + try: + fileno = f.fileno() + except (OSError, io.UnsupportedOperation): + return False + + handle = msvcrt.get_osfhandle(fileno) + return bool(GetConsoleMode(handle, byref(DWORD()))) + + +def _get_windows_console_stream( + f: t.TextIO, encoding: str | None, errors: str | None +) -> t.TextIO | None: + if ( + get_buffer is None + or encoding not in {"utf-16-le", None} + or errors not in {"strict", None} + or not _is_console(f) + ): + return None + + func = _stream_factories.get(f.fileno()) + if func is None: + return None + + b = getattr(f, "buffer", None) + + if b is None: + return None + + return func(b) diff --git a/netdeploy/lib/python3.11/site-packages/click/core.py b/netdeploy/lib/python3.11/site-packages/click/core.py new file mode 100644 index 0000000..ff2f74a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/core.py @@ -0,0 +1,3347 @@ +from __future__ import annotations + +import collections.abc as cabc +import enum +import errno +import inspect +import os +import sys +import typing as t +from collections import abc +from collections import Counter +from contextlib import AbstractContextManager +from contextlib import contextmanager +from contextlib import ExitStack +from functools import update_wrapper +from gettext import gettext as _ +from gettext import ngettext +from itertools import repeat +from types import TracebackType + +from . import types +from ._utils import FLAG_NEEDS_VALUE +from ._utils import UNSET +from .exceptions import Abort +from .exceptions import BadParameter +from .exceptions import ClickException +from .exceptions import Exit +from .exceptions import MissingParameter +from .exceptions import NoArgsIsHelpError +from .exceptions import UsageError +from .formatting import HelpFormatter +from .formatting import join_options +from .globals import pop_context +from .globals import push_context +from .parser import _OptionParser +from .parser import _split_opt +from .termui import confirm +from .termui import prompt +from .termui import style +from .utils import _detect_program_name +from .utils import _expand_args +from .utils import echo +from .utils import make_default_short_help +from .utils import make_str +from .utils import PacifyFlushWrapper + +if t.TYPE_CHECKING: + from .shell_completion import CompletionItem + +F = t.TypeVar("F", bound="t.Callable[..., t.Any]") +V = t.TypeVar("V") + + +def _complete_visible_commands( + ctx: Context, incomplete: str +) -> cabc.Iterator[tuple[str, Command]]: + """List all the subcommands of a group that start with the + incomplete value and aren't hidden. + + :param ctx: Invocation context for the group. + :param incomplete: Value being completed. May be empty. + """ + multi = t.cast(Group, ctx.command) + + for name in multi.list_commands(ctx): + if name.startswith(incomplete): + command = multi.get_command(ctx, name) + + if command is not None and not command.hidden: + yield name, command + + +def _check_nested_chain( + base_command: Group, cmd_name: str, cmd: Command, register: bool = False +) -> None: + if not base_command.chain or not isinstance(cmd, Group): + return + + if register: + message = ( + f"It is not possible to add the group {cmd_name!r} to another" + f" group {base_command.name!r} that is in chain mode." + ) + else: + message = ( + f"Found the group {cmd_name!r} as subcommand to another group " + f" {base_command.name!r} that is in chain mode. This is not supported." + ) + + raise RuntimeError(message) + + +def batch(iterable: cabc.Iterable[V], batch_size: int) -> list[tuple[V, ...]]: + return list(zip(*repeat(iter(iterable), batch_size), strict=False)) + + +@contextmanager +def augment_usage_errors( + ctx: Context, param: Parameter | None = None +) -> cabc.Iterator[None]: + """Context manager that attaches extra information to exceptions.""" + try: + yield + except BadParameter as e: + if e.ctx is None: + e.ctx = ctx + if param is not None and e.param is None: + e.param = param + raise + except UsageError as e: + if e.ctx is None: + e.ctx = ctx + raise + + +def iter_params_for_processing( + invocation_order: cabc.Sequence[Parameter], + declaration_order: cabc.Sequence[Parameter], +) -> list[Parameter]: + """Returns all declared parameters in the order they should be processed. + + The declared parameters are re-shuffled depending on the order in which + they were invoked, as well as the eagerness of each parameters. + + The invocation order takes precedence over the declaration order. I.e. the + order in which the user provided them to the CLI is respected. + + This behavior and its effect on callback evaluation is detailed at: + https://click.palletsprojects.com/en/stable/advanced/#callback-evaluation-order + """ + + def sort_key(item: Parameter) -> tuple[bool, float]: + try: + idx: float = invocation_order.index(item) + except ValueError: + idx = float("inf") + + return not item.is_eager, idx + + return sorted(declaration_order, key=sort_key) + + +class ParameterSource(enum.Enum): + """This is an :class:`~enum.Enum` that indicates the source of a + parameter's value. + + Use :meth:`click.Context.get_parameter_source` to get the + source for a parameter by name. + + .. versionchanged:: 8.0 + Use :class:`~enum.Enum` and drop the ``validate`` method. + + .. versionchanged:: 8.0 + Added the ``PROMPT`` value. + """ + + COMMANDLINE = enum.auto() + """The value was provided by the command line args.""" + ENVIRONMENT = enum.auto() + """The value was provided with an environment variable.""" + DEFAULT = enum.auto() + """Used the default specified by the parameter.""" + DEFAULT_MAP = enum.auto() + """Used a default provided by :attr:`Context.default_map`.""" + PROMPT = enum.auto() + """Used a prompt to confirm a default or provide a value.""" + + +class Context: + """The context is a special internal object that holds state relevant + for the script execution at every single level. It's normally invisible + to commands unless they opt-in to getting access to it. + + The context is useful as it can pass internal objects around and can + control special execution features such as reading data from + environment variables. + + A context can be used as context manager in which case it will call + :meth:`close` on teardown. + + :param command: the command class for this context. + :param parent: the parent context. + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it is usually + the name of the script, for commands below it it's + the name of the script. + :param obj: an arbitrary object of user data. + :param auto_envvar_prefix: the prefix to use for automatic environment + variables. If this is `None` then reading + from environment variables is disabled. This + does not affect manually set environment + variables which are always read. + :param default_map: a dictionary (like object) with default values + for parameters. + :param terminal_width: the width of the terminal. The default is + inherit from parent context. If no context + defines the terminal width then auto + detection will be applied. + :param max_content_width: the maximum width for content rendered by + Click (this currently only affects help + pages). This defaults to 80 characters if + not overridden. In other words: even if the + terminal is larger than that, Click will not + format things wider than 80 characters by + default. In addition to that, formatters might + add some safety mapping on the right. + :param resilient_parsing: if this flag is enabled then Click will + parse without any interactivity or callback + invocation. Default values will also be + ignored. This is useful for implementing + things such as completion support. + :param allow_extra_args: if this is set to `True` then extra arguments + at the end will not raise an error and will be + kept on the context. The default is to inherit + from the command. + :param allow_interspersed_args: if this is set to `False` then options + and arguments cannot be mixed. The + default is to inherit from the command. + :param ignore_unknown_options: instructs click to ignore options it does + not know and keeps them for later + processing. + :param help_option_names: optionally a list of strings that define how + the default help parameter is named. The + default is ``['--help']``. + :param token_normalize_func: an optional function that is used to + normalize tokens (options, choices, + etc.). This for instance can be used to + implement case insensitive behavior. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are used in texts that Click prints which is by + default not the case. This for instance would affect + help output. + :param show_default: Show the default value for commands. If this + value is not set, it defaults to the value from the parent + context. ``Command.show_default`` overrides this default for the + specific command. + + .. versionchanged:: 8.2 + The ``protected_args`` attribute is deprecated and will be removed in + Click 9.0. ``args`` will contain remaining unparsed tokens. + + .. versionchanged:: 8.1 + The ``show_default`` parameter is overridden by + ``Command.show_default``, instead of the other way around. + + .. versionchanged:: 8.0 + The ``show_default`` parameter defaults to the value from the + parent context. + + .. versionchanged:: 7.1 + Added the ``show_default`` parameter. + + .. versionchanged:: 4.0 + Added the ``color``, ``ignore_unknown_options``, and + ``max_content_width`` parameters. + + .. versionchanged:: 3.0 + Added the ``allow_extra_args`` and ``allow_interspersed_args`` + parameters. + + .. versionchanged:: 2.0 + Added the ``resilient_parsing``, ``help_option_names``, and + ``token_normalize_func`` parameters. + """ + + #: The formatter class to create with :meth:`make_formatter`. + #: + #: .. versionadded:: 8.0 + formatter_class: type[HelpFormatter] = HelpFormatter + + def __init__( + self, + command: Command, + parent: Context | None = None, + info_name: str | None = None, + obj: t.Any | None = None, + auto_envvar_prefix: str | None = None, + default_map: cabc.MutableMapping[str, t.Any] | None = None, + terminal_width: int | None = None, + max_content_width: int | None = None, + resilient_parsing: bool = False, + allow_extra_args: bool | None = None, + allow_interspersed_args: bool | None = None, + ignore_unknown_options: bool | None = None, + help_option_names: list[str] | None = None, + token_normalize_func: t.Callable[[str], str] | None = None, + color: bool | None = None, + show_default: bool | None = None, + ) -> None: + #: the parent context or `None` if none exists. + self.parent = parent + #: the :class:`Command` for this context. + self.command = command + #: the descriptive information name + self.info_name = info_name + #: Map of parameter names to their parsed values. Parameters + #: with ``expose_value=False`` are not stored. + self.params: dict[str, t.Any] = {} + #: the leftover arguments. + self.args: list[str] = [] + #: protected arguments. These are arguments that are prepended + #: to `args` when certain parsing scenarios are encountered but + #: must be never propagated to another arguments. This is used + #: to implement nested parsing. + self._protected_args: list[str] = [] + #: the collected prefixes of the command's options. + self._opt_prefixes: set[str] = set(parent._opt_prefixes) if parent else set() + + if obj is None and parent is not None: + obj = parent.obj + + #: the user object stored. + self.obj: t.Any = obj + self._meta: dict[str, t.Any] = getattr(parent, "meta", {}) + + #: A dictionary (-like object) with defaults for parameters. + if ( + default_map is None + and info_name is not None + and parent is not None + and parent.default_map is not None + ): + default_map = parent.default_map.get(info_name) + + self.default_map: cabc.MutableMapping[str, t.Any] | None = default_map + + #: This flag indicates if a subcommand is going to be executed. A + #: group callback can use this information to figure out if it's + #: being executed directly or because the execution flow passes + #: onwards to a subcommand. By default it's None, but it can be + #: the name of the subcommand to execute. + #: + #: If chaining is enabled this will be set to ``'*'`` in case + #: any commands are executed. It is however not possible to + #: figure out which ones. If you require this knowledge you + #: should use a :func:`result_callback`. + self.invoked_subcommand: str | None = None + + if terminal_width is None and parent is not None: + terminal_width = parent.terminal_width + + #: The width of the terminal (None is autodetection). + self.terminal_width: int | None = terminal_width + + if max_content_width is None and parent is not None: + max_content_width = parent.max_content_width + + #: The maximum width of formatted content (None implies a sensible + #: default which is 80 for most things). + self.max_content_width: int | None = max_content_width + + if allow_extra_args is None: + allow_extra_args = command.allow_extra_args + + #: Indicates if the context allows extra args or if it should + #: fail on parsing. + #: + #: .. versionadded:: 3.0 + self.allow_extra_args = allow_extra_args + + if allow_interspersed_args is None: + allow_interspersed_args = command.allow_interspersed_args + + #: Indicates if the context allows mixing of arguments and + #: options or not. + #: + #: .. versionadded:: 3.0 + self.allow_interspersed_args: bool = allow_interspersed_args + + if ignore_unknown_options is None: + ignore_unknown_options = command.ignore_unknown_options + + #: Instructs click to ignore options that a command does not + #: understand and will store it on the context for later + #: processing. This is primarily useful for situations where you + #: want to call into external programs. Generally this pattern is + #: strongly discouraged because it's not possibly to losslessly + #: forward all arguments. + #: + #: .. versionadded:: 4.0 + self.ignore_unknown_options: bool = ignore_unknown_options + + if help_option_names is None: + if parent is not None: + help_option_names = parent.help_option_names + else: + help_option_names = ["--help"] + + #: The names for the help options. + self.help_option_names: list[str] = help_option_names + + if token_normalize_func is None and parent is not None: + token_normalize_func = parent.token_normalize_func + + #: An optional normalization function for tokens. This is + #: options, choices, commands etc. + self.token_normalize_func: t.Callable[[str], str] | None = token_normalize_func + + #: Indicates if resilient parsing is enabled. In that case Click + #: will do its best to not cause any failures and default values + #: will be ignored. Useful for completion. + self.resilient_parsing: bool = resilient_parsing + + # If there is no envvar prefix yet, but the parent has one and + # the command on this level has a name, we can expand the envvar + # prefix automatically. + if auto_envvar_prefix is None: + if ( + parent is not None + and parent.auto_envvar_prefix is not None + and self.info_name is not None + ): + auto_envvar_prefix = ( + f"{parent.auto_envvar_prefix}_{self.info_name.upper()}" + ) + else: + auto_envvar_prefix = auto_envvar_prefix.upper() + + if auto_envvar_prefix is not None: + auto_envvar_prefix = auto_envvar_prefix.replace("-", "_") + + self.auto_envvar_prefix: str | None = auto_envvar_prefix + + if color is None and parent is not None: + color = parent.color + + #: Controls if styling output is wanted or not. + self.color: bool | None = color + + if show_default is None and parent is not None: + show_default = parent.show_default + + #: Show option default values when formatting help text. + self.show_default: bool | None = show_default + + self._close_callbacks: list[t.Callable[[], t.Any]] = [] + self._depth = 0 + self._parameter_source: dict[str, ParameterSource] = {} + self._exit_stack = ExitStack() + + @property + def protected_args(self) -> list[str]: + import warnings + + warnings.warn( + "'protected_args' is deprecated and will be removed in Click 9.0." + " 'args' will contain remaining unparsed tokens.", + DeprecationWarning, + stacklevel=2, + ) + return self._protected_args + + def to_info_dict(self) -> dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. This traverses the entire CLI + structure. + + .. code-block:: python + + with Context(cli) as ctx: + info = ctx.to_info_dict() + + .. versionadded:: 8.0 + """ + return { + "command": self.command.to_info_dict(self), + "info_name": self.info_name, + "allow_extra_args": self.allow_extra_args, + "allow_interspersed_args": self.allow_interspersed_args, + "ignore_unknown_options": self.ignore_unknown_options, + "auto_envvar_prefix": self.auto_envvar_prefix, + } + + def __enter__(self) -> Context: + self._depth += 1 + push_context(self) + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> bool | None: + self._depth -= 1 + exit_result: bool | None = None + if self._depth == 0: + exit_result = self._close_with_exception_info(exc_type, exc_value, tb) + pop_context() + + return exit_result + + @contextmanager + def scope(self, cleanup: bool = True) -> cabc.Iterator[Context]: + """This helper method can be used with the context object to promote + it to the current thread local (see :func:`get_current_context`). + The default behavior of this is to invoke the cleanup functions which + can be disabled by setting `cleanup` to `False`. The cleanup + functions are typically used for things such as closing file handles. + + If the cleanup is intended the context object can also be directly + used as a context manager. + + Example usage:: + + with ctx.scope(): + assert get_current_context() is ctx + + This is equivalent:: + + with ctx: + assert get_current_context() is ctx + + .. versionadded:: 5.0 + + :param cleanup: controls if the cleanup functions should be run or + not. The default is to run these functions. In + some situations the context only wants to be + temporarily pushed in which case this can be disabled. + Nested pushes automatically defer the cleanup. + """ + if not cleanup: + self._depth += 1 + try: + with self as rv: + yield rv + finally: + if not cleanup: + self._depth -= 1 + + @property + def meta(self) -> dict[str, t.Any]: + """This is a dictionary which is shared with all the contexts + that are nested. It exists so that click utilities can store some + state here if they need to. It is however the responsibility of + that code to manage this dictionary well. + + The keys are supposed to be unique dotted strings. For instance + module paths are a good choice for it. What is stored in there is + irrelevant for the operation of click. However what is important is + that code that places data here adheres to the general semantics of + the system. + + Example usage:: + + LANG_KEY = f'{__name__}.lang' + + def set_language(value): + ctx = get_current_context() + ctx.meta[LANG_KEY] = value + + def get_language(): + return get_current_context().meta.get(LANG_KEY, 'en_US') + + .. versionadded:: 5.0 + """ + return self._meta + + def make_formatter(self) -> HelpFormatter: + """Creates the :class:`~click.HelpFormatter` for the help and + usage output. + + To quickly customize the formatter class used without overriding + this method, set the :attr:`formatter_class` attribute. + + .. versionchanged:: 8.0 + Added the :attr:`formatter_class` attribute. + """ + return self.formatter_class( + width=self.terminal_width, max_width=self.max_content_width + ) + + def with_resource(self, context_manager: AbstractContextManager[V]) -> V: + """Register a resource as if it were used in a ``with`` + statement. The resource will be cleaned up when the context is + popped. + + Uses :meth:`contextlib.ExitStack.enter_context`. It calls the + resource's ``__enter__()`` method and returns the result. When + the context is popped, it closes the stack, which calls the + resource's ``__exit__()`` method. + + To register a cleanup function for something that isn't a + context manager, use :meth:`call_on_close`. Or use something + from :mod:`contextlib` to turn it into a context manager first. + + .. code-block:: python + + @click.group() + @click.option("--name") + @click.pass_context + def cli(ctx): + ctx.obj = ctx.with_resource(connect_db(name)) + + :param context_manager: The context manager to enter. + :return: Whatever ``context_manager.__enter__()`` returns. + + .. versionadded:: 8.0 + """ + return self._exit_stack.enter_context(context_manager) + + def call_on_close(self, f: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + """Register a function to be called when the context tears down. + + This can be used to close resources opened during the script + execution. Resources that support Python's context manager + protocol which would be used in a ``with`` statement should be + registered with :meth:`with_resource` instead. + + :param f: The function to execute on teardown. + """ + return self._exit_stack.callback(f) + + def close(self) -> None: + """Invoke all close callbacks registered with + :meth:`call_on_close`, and exit all context managers entered + with :meth:`with_resource`. + """ + self._close_with_exception_info(None, None, None) + + def _close_with_exception_info( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> bool | None: + """Unwind the exit stack by calling its :meth:`__exit__` providing the exception + information to allow for exception handling by the various resources registered + using :meth;`with_resource` + + :return: Whatever ``exit_stack.__exit__()`` returns. + """ + exit_result = self._exit_stack.__exit__(exc_type, exc_value, tb) + # In case the context is reused, create a new exit stack. + self._exit_stack = ExitStack() + + return exit_result + + @property + def command_path(self) -> str: + """The computed command path. This is used for the ``usage`` + information on the help page. It's automatically created by + combining the info names of the chain of contexts to the root. + """ + rv = "" + if self.info_name is not None: + rv = self.info_name + if self.parent is not None: + parent_command_path = [self.parent.command_path] + + if isinstance(self.parent.command, Command): + for param in self.parent.command.get_params(self): + parent_command_path.extend(param.get_usage_pieces(self)) + + rv = f"{' '.join(parent_command_path)} {rv}" + return rv.lstrip() + + def find_root(self) -> Context: + """Finds the outermost context.""" + node = self + while node.parent is not None: + node = node.parent + return node + + def find_object(self, object_type: type[V]) -> V | None: + """Finds the closest object of a given type.""" + node: Context | None = self + + while node is not None: + if isinstance(node.obj, object_type): + return node.obj + + node = node.parent + + return None + + def ensure_object(self, object_type: type[V]) -> V: + """Like :meth:`find_object` but sets the innermost object to a + new instance of `object_type` if it does not exist. + """ + rv = self.find_object(object_type) + if rv is None: + self.obj = rv = object_type() + return rv + + @t.overload + def lookup_default( + self, name: str, call: t.Literal[True] = True + ) -> t.Any | None: ... + + @t.overload + def lookup_default( + self, name: str, call: t.Literal[False] = ... + ) -> t.Any | t.Callable[[], t.Any] | None: ... + + def lookup_default(self, name: str, call: bool = True) -> t.Any | None: + """Get the default for a parameter from :attr:`default_map`. + + :param name: Name of the parameter. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + if self.default_map is not None: + value = self.default_map.get(name, UNSET) + + if call and callable(value): + return value() + + return value + + return UNSET + + def fail(self, message: str) -> t.NoReturn: + """Aborts the execution of the program with a specific error + message. + + :param message: the error message to fail with. + """ + raise UsageError(message, self) + + def abort(self) -> t.NoReturn: + """Aborts the script.""" + raise Abort() + + def exit(self, code: int = 0) -> t.NoReturn: + """Exits the application with a given exit code. + + .. versionchanged:: 8.2 + Callbacks and context managers registered with :meth:`call_on_close` + and :meth:`with_resource` are closed before exiting. + """ + self.close() + raise Exit(code) + + def get_usage(self) -> str: + """Helper method to get formatted usage string for the current + context and command. + """ + return self.command.get_usage(self) + + def get_help(self) -> str: + """Helper method to get formatted help page for the current + context and command. + """ + return self.command.get_help(self) + + def _make_sub_context(self, command: Command) -> Context: + """Create a new context of the same type as this context, but + for a new command. + + :meta private: + """ + return type(self)(command, info_name=command.name, parent=self) + + @t.overload + def invoke( + self, callback: t.Callable[..., V], /, *args: t.Any, **kwargs: t.Any + ) -> V: ... + + @t.overload + def invoke(self, callback: Command, /, *args: t.Any, **kwargs: t.Any) -> t.Any: ... + + def invoke( + self, callback: Command | t.Callable[..., V], /, *args: t.Any, **kwargs: t.Any + ) -> t.Any | V: + """Invokes a command callback in exactly the way it expects. There + are two ways to invoke this method: + + 1. the first argument can be a callback and all other arguments and + keyword arguments are forwarded directly to the function. + 2. the first argument is a click command object. In that case all + arguments are forwarded as well but proper click parameters + (options and click arguments) must be keyword arguments and Click + will fill in defaults. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if :meth:`forward` is called at multiple levels. + + .. versionchanged:: 3.2 + A new context is created, and missing arguments use default values. + """ + if isinstance(callback, Command): + other_cmd = callback + + if other_cmd.callback is None: + raise TypeError( + "The given command does not have a callback that can be invoked." + ) + else: + callback = t.cast("t.Callable[..., V]", other_cmd.callback) + + ctx = self._make_sub_context(other_cmd) + + for param in other_cmd.params: + if param.name not in kwargs and param.expose_value: + kwargs[param.name] = param.type_cast_value( # type: ignore + ctx, param.get_default(ctx) + ) + + # Track all kwargs as params, so that forward() will pass + # them on in subsequent calls. + ctx.params.update(kwargs) + else: + ctx = self + + with augment_usage_errors(self): + with ctx: + return callback(*args, **kwargs) + + def forward(self, cmd: Command, /, *args: t.Any, **kwargs: t.Any) -> t.Any: + """Similar to :meth:`invoke` but fills in default keyword + arguments from the current context if the other command expects + it. This cannot invoke callbacks directly, only other commands. + + .. versionchanged:: 8.0 + All ``kwargs`` are tracked in :attr:`params` so they will be + passed if ``forward`` is called at multiple levels. + """ + # Can only forward to other commands, not direct callbacks. + if not isinstance(cmd, Command): + raise TypeError("Callback is not a command.") + + for param in self.params: + if param not in kwargs: + kwargs[param] = self.params[param] + + return self.invoke(cmd, *args, **kwargs) + + def set_parameter_source(self, name: str, source: ParameterSource) -> None: + """Set the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + :param name: The name of the parameter. + :param source: A member of :class:`~click.core.ParameterSource`. + """ + self._parameter_source[name] = source + + def get_parameter_source(self, name: str) -> ParameterSource | None: + """Get the source of a parameter. This indicates the location + from which the value of the parameter was obtained. + + This can be useful for determining when a user specified a value + on the command line that is the same as the default value. It + will be :attr:`~click.core.ParameterSource.DEFAULT` only if the + value was actually taken from the default. + + :param name: The name of the parameter. + :rtype: ParameterSource + + .. versionchanged:: 8.0 + Returns ``None`` if the parameter was not provided from any + source. + """ + return self._parameter_source.get(name) + + +class Command: + """Commands are the basic building block of command line interfaces in + Click. A basic command handles command line parsing and might dispatch + more parsing to commands nested below it. + + :param name: the name of the command to use unless a group overrides it. + :param context_settings: an optional dictionary with defaults that are + passed to the context object. + :param callback: the callback to invoke. This is optional. + :param params: the parameters to register with this command. This can + be either :class:`Option` or :class:`Argument` objects. + :param help: the help string to use for this command. + :param epilog: like the help string but it's printed at the end of the + help page after everything else. + :param short_help: the short help to use for this command. This is + shown on the command listing of the parent command. + :param add_help_option: by default each command registers a ``--help`` + option. This can be disabled by this parameter. + :param no_args_is_help: this controls what happens if no arguments are + provided. This option is disabled by default. + If enabled this will add ``--help`` as argument + if no arguments are passed + :param hidden: hide this command from help outputs. + :param deprecated: If ``True`` or non-empty string, issues a message + indicating that the command is deprecated and highlights + its deprecation in --help. The message can be customized + by using a string as the value. + + .. versionchanged:: 8.2 + This is the base class for all commands, not ``BaseCommand``. + ``deprecated`` can be set to a string as well to customize the + deprecation message. + + .. versionchanged:: 8.1 + ``help``, ``epilog``, and ``short_help`` are stored unprocessed, + all formatting is done when outputting help text, not at init, + and is done even if not using the ``@command`` decorator. + + .. versionchanged:: 8.0 + Added a ``repr`` showing the command name. + + .. versionchanged:: 7.1 + Added the ``no_args_is_help`` parameter. + + .. versionchanged:: 2.0 + Added the ``context_settings`` parameter. + """ + + #: The context class to create with :meth:`make_context`. + #: + #: .. versionadded:: 8.0 + context_class: type[Context] = Context + + #: the default for the :attr:`Context.allow_extra_args` flag. + allow_extra_args = False + + #: the default for the :attr:`Context.allow_interspersed_args` flag. + allow_interspersed_args = True + + #: the default for the :attr:`Context.ignore_unknown_options` flag. + ignore_unknown_options = False + + def __init__( + self, + name: str | None, + context_settings: cabc.MutableMapping[str, t.Any] | None = None, + callback: t.Callable[..., t.Any] | None = None, + params: list[Parameter] | None = None, + help: str | None = None, + epilog: str | None = None, + short_help: str | None = None, + options_metavar: str | None = "[OPTIONS]", + add_help_option: bool = True, + no_args_is_help: bool = False, + hidden: bool = False, + deprecated: bool | str = False, + ) -> None: + #: the name the command thinks it has. Upon registering a command + #: on a :class:`Group` the group will default the command name + #: with this information. You should instead use the + #: :class:`Context`\'s :attr:`~Context.info_name` attribute. + self.name = name + + if context_settings is None: + context_settings = {} + + #: an optional dictionary with defaults passed to the context. + self.context_settings: cabc.MutableMapping[str, t.Any] = context_settings + + #: the callback to execute when the command fires. This might be + #: `None` in which case nothing happens. + self.callback = callback + #: the list of parameters for this command in the order they + #: should show up in the help page and execute. Eager parameters + #: will automatically be handled before non eager ones. + self.params: list[Parameter] = params or [] + self.help = help + self.epilog = epilog + self.options_metavar = options_metavar + self.short_help = short_help + self.add_help_option = add_help_option + self._help_option = None + self.no_args_is_help = no_args_is_help + self.hidden = hidden + self.deprecated = deprecated + + def to_info_dict(self, ctx: Context) -> dict[str, t.Any]: + return { + "name": self.name, + "params": [param.to_info_dict() for param in self.get_params(ctx)], + "help": self.help, + "epilog": self.epilog, + "short_help": self.short_help, + "hidden": self.hidden, + "deprecated": self.deprecated, + } + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def get_usage(self, ctx: Context) -> str: + """Formats the usage line into a string and returns it. + + Calls :meth:`format_usage` internally. + """ + formatter = ctx.make_formatter() + self.format_usage(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_params(self, ctx: Context) -> list[Parameter]: + params = self.params + help_option = self.get_help_option(ctx) + + if help_option is not None: + params = [*params, help_option] + + if __debug__: + import warnings + + opts = [opt for param in params for opt in param.opts] + opts_counter = Counter(opts) + duplicate_opts = (opt for opt, count in opts_counter.items() if count > 1) + + for duplicate_opt in duplicate_opts: + warnings.warn( + ( + f"The parameter {duplicate_opt} is used more than once. " + "Remove its duplicate as parameters should be unique." + ), + stacklevel=3, + ) + + return params + + def format_usage(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the usage line into the formatter. + + This is a low-level method called by :meth:`get_usage`. + """ + pieces = self.collect_usage_pieces(ctx) + formatter.write_usage(ctx.command_path, " ".join(pieces)) + + def collect_usage_pieces(self, ctx: Context) -> list[str]: + """Returns all the pieces that go into the usage line and returns + it as a list of strings. + """ + rv = [self.options_metavar] if self.options_metavar else [] + + for param in self.get_params(ctx): + rv.extend(param.get_usage_pieces(ctx)) + + return rv + + def get_help_option_names(self, ctx: Context) -> list[str]: + """Returns the names for the help option.""" + all_names = set(ctx.help_option_names) + for param in self.params: + all_names.difference_update(param.opts) + all_names.difference_update(param.secondary_opts) + return list(all_names) + + def get_help_option(self, ctx: Context) -> Option | None: + """Returns the help option object. + + Skipped if :attr:`add_help_option` is ``False``. + + .. versionchanged:: 8.1.8 + The help option is now cached to avoid creating it multiple times. + """ + help_option_names = self.get_help_option_names(ctx) + + if not help_option_names or not self.add_help_option: + return None + + # Cache the help option object in private _help_option attribute to + # avoid creating it multiple times. Not doing this will break the + # callback odering by iter_params_for_processing(), which relies on + # object comparison. + if self._help_option is None: + # Avoid circular import. + from .decorators import help_option + + # Apply help_option decorator and pop resulting option + help_option(*help_option_names)(self) + self._help_option = self.params.pop() # type: ignore[assignment] + + return self._help_option + + def make_parser(self, ctx: Context) -> _OptionParser: + """Creates the underlying option parser for this command.""" + parser = _OptionParser(ctx) + for param in self.get_params(ctx): + param.add_to_parser(parser, ctx) + return parser + + def get_help(self, ctx: Context) -> str: + """Formats the help into a string and returns it. + + Calls :meth:`format_help` internally. + """ + formatter = ctx.make_formatter() + self.format_help(ctx, formatter) + return formatter.getvalue().rstrip("\n") + + def get_short_help_str(self, limit: int = 45) -> str: + """Gets short help for the command or makes it by shortening the + long help string. + """ + if self.short_help: + text = inspect.cleandoc(self.short_help) + elif self.help: + text = make_default_short_help(self.help, limit) + else: + text = "" + + if self.deprecated: + deprecated_message = ( + f"(DEPRECATED: {self.deprecated})" + if isinstance(self.deprecated, str) + else "(DEPRECATED)" + ) + text = _("{text} {deprecated_message}").format( + text=text, deprecated_message=deprecated_message + ) + + return text.strip() + + def format_help(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the help into the formatter if it exists. + + This is a low-level method called by :meth:`get_help`. + + This calls the following methods: + + - :meth:`format_usage` + - :meth:`format_help_text` + - :meth:`format_options` + - :meth:`format_epilog` + """ + self.format_usage(ctx, formatter) + self.format_help_text(ctx, formatter) + self.format_options(ctx, formatter) + self.format_epilog(ctx, formatter) + + def format_help_text(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the help text to the formatter if it exists.""" + if self.help is not None: + # truncate the help text to the first form feed + text = inspect.cleandoc(self.help).partition("\f")[0] + else: + text = "" + + if self.deprecated: + deprecated_message = ( + f"(DEPRECATED: {self.deprecated})" + if isinstance(self.deprecated, str) + else "(DEPRECATED)" + ) + text = _("{text} {deprecated_message}").format( + text=text, deprecated_message=deprecated_message + ) + + if text: + formatter.write_paragraph() + + with formatter.indentation(): + formatter.write_text(text) + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes all the options into the formatter if they exist.""" + opts = [] + for param in self.get_params(ctx): + rv = param.get_help_record(ctx) + if rv is not None: + opts.append(rv) + + if opts: + with formatter.section(_("Options")): + formatter.write_dl(opts) + + def format_epilog(self, ctx: Context, formatter: HelpFormatter) -> None: + """Writes the epilog into the formatter if it exists.""" + if self.epilog: + epilog = inspect.cleandoc(self.epilog) + formatter.write_paragraph() + + with formatter.indentation(): + formatter.write_text(epilog) + + def make_context( + self, + info_name: str | None, + args: list[str], + parent: Context | None = None, + **extra: t.Any, + ) -> Context: + """This function when given an info name and arguments will kick + off the parsing and create a new :class:`Context`. It does not + invoke the actual command callback though. + + To quickly customize the context class used without overriding + this method, set the :attr:`context_class` attribute. + + :param info_name: the info name for this invocation. Generally this + is the most descriptive name for the script or + command. For the toplevel script it's usually + the name of the script, for commands below it's + the name of the command. + :param args: the arguments to parse as list of strings. + :param parent: the parent context if available. + :param extra: extra keyword arguments forwarded to the context + constructor. + + .. versionchanged:: 8.0 + Added the :attr:`context_class` attribute. + """ + for key, value in self.context_settings.items(): + if key not in extra: + extra[key] = value + + ctx = self.context_class(self, info_name=info_name, parent=parent, **extra) + + with ctx.scope(cleanup=False): + self.parse_args(ctx, args) + return ctx + + def parse_args(self, ctx: Context, args: list[str]) -> list[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + raise NoArgsIsHelpError(ctx) + + parser = self.make_parser(ctx) + opts, args, param_order = parser.parse_args(args=args) + + for param in iter_params_for_processing(param_order, self.get_params(ctx)): + _, args = param.handle_parse_result(ctx, opts, args) + + if args and not ctx.allow_extra_args and not ctx.resilient_parsing: + ctx.fail( + ngettext( + "Got unexpected extra argument ({args})", + "Got unexpected extra arguments ({args})", + len(args), + ).format(args=" ".join(map(str, args))) + ) + + ctx.args = args + ctx._opt_prefixes.update(parser._opt_prefixes) + return args + + def invoke(self, ctx: Context) -> t.Any: + """Given a context, this invokes the attached callback (if it exists) + in the right way. + """ + if self.deprecated: + extra_message = ( + f" {self.deprecated}" if isinstance(self.deprecated, str) else "" + ) + message = _( + "DeprecationWarning: The command {name!r} is deprecated.{extra_message}" + ).format(name=self.name, extra_message=extra_message) + echo(style(message, fg="red"), err=True) + + if self.callback is not None: + return ctx.invoke(self.callback, **ctx.params) + + def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: + """Return a list of completions for the incomplete value. Looks + at the names of options and chained multi-commands. + + Any command could be part of a chained multi-command, so sibling + commands are valid at any point during command completion. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results: list[CompletionItem] = [] + + if incomplete and not incomplete[0].isalnum(): + for param in self.get_params(ctx): + if ( + not isinstance(param, Option) + or param.hidden + or ( + not param.multiple + and ctx.get_parameter_source(param.name) # type: ignore + is ParameterSource.COMMANDLINE + ) + ): + continue + + results.extend( + CompletionItem(name, help=param.help) + for name in [*param.opts, *param.secondary_opts] + if name.startswith(incomplete) + ) + + while ctx.parent is not None: + ctx = ctx.parent + + if isinstance(ctx.command, Group) and ctx.command.chain: + results.extend( + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + if name not in ctx._protected_args + ) + + return results + + @t.overload + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: t.Literal[True] = True, + **extra: t.Any, + ) -> t.NoReturn: ... + + @t.overload + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: bool = ..., + **extra: t.Any, + ) -> t.Any: ... + + def main( + self, + args: cabc.Sequence[str] | None = None, + prog_name: str | None = None, + complete_var: str | None = None, + standalone_mode: bool = True, + windows_expand_args: bool = True, + **extra: t.Any, + ) -> t.Any: + """This is the way to invoke a script with all the bells and + whistles as a command line application. This will always terminate + the application after a call. If this is not wanted, ``SystemExit`` + needs to be caught. + + This method is also available by directly calling the instance of + a :class:`Command`. + + :param args: the arguments that should be used for parsing. If not + provided, ``sys.argv[1:]`` is used. + :param prog_name: the program name that should be used. By default + the program name is constructed by taking the file + name from ``sys.argv[0]``. + :param complete_var: the environment variable that controls the + bash completion support. The default is + ``"__COMPLETE"`` with prog_name in + uppercase. + :param standalone_mode: the default behavior is to invoke the script + in standalone mode. Click will then + handle exceptions and convert them into + error messages and the function will never + return but shut down the interpreter. If + this is set to `False` they will be + propagated to the caller and the return + value of this function is the return value + of :meth:`invoke`. + :param windows_expand_args: Expand glob patterns, user dir, and + env vars in command line args on Windows. + :param extra: extra keyword arguments are forwarded to the context + constructor. See :class:`Context` for more information. + + .. versionchanged:: 8.0.1 + Added the ``windows_expand_args`` parameter to allow + disabling command line arg expansion on Windows. + + .. versionchanged:: 8.0 + When taking arguments from ``sys.argv`` on Windows, glob + patterns, user dir, and env vars are expanded. + + .. versionchanged:: 3.0 + Added the ``standalone_mode`` parameter. + """ + if args is None: + args = sys.argv[1:] + + if os.name == "nt" and windows_expand_args: + args = _expand_args(args) + else: + args = list(args) + + if prog_name is None: + prog_name = _detect_program_name() + + # Process shell completion requests and exit early. + self._main_shell_completion(extra, prog_name, complete_var) + + try: + try: + with self.make_context(prog_name, args, **extra) as ctx: + rv = self.invoke(ctx) + if not standalone_mode: + return rv + # it's not safe to `ctx.exit(rv)` here! + # note that `rv` may actually contain data like "1" which + # has obvious effects + # more subtle case: `rv=[None, None]` can come out of + # chained commands which all returned `None` -- so it's not + # even always obvious that `rv` indicates success/failure + # by its truthiness/falsiness + ctx.exit() + except (EOFError, KeyboardInterrupt) as e: + echo(file=sys.stderr) + raise Abort() from e + except ClickException as e: + if not standalone_mode: + raise + e.show() + sys.exit(e.exit_code) + except OSError as e: + if e.errno == errno.EPIPE: + sys.stdout = t.cast(t.TextIO, PacifyFlushWrapper(sys.stdout)) + sys.stderr = t.cast(t.TextIO, PacifyFlushWrapper(sys.stderr)) + sys.exit(1) + else: + raise + except Exit as e: + if standalone_mode: + sys.exit(e.exit_code) + else: + # in non-standalone mode, return the exit code + # note that this is only reached if `self.invoke` above raises + # an Exit explicitly -- thus bypassing the check there which + # would return its result + # the results of non-standalone execution may therefore be + # somewhat ambiguous: if there are codepaths which lead to + # `ctx.exit(1)` and to `return 1`, the caller won't be able to + # tell the difference between the two + return e.exit_code + except Abort: + if not standalone_mode: + raise + echo(_("Aborted!"), file=sys.stderr) + sys.exit(1) + + def _main_shell_completion( + self, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str | None = None, + ) -> None: + """Check if the shell is asking for tab completion, process + that, then exit early. Called from :meth:`main` before the + program is invoked. + + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. Defaults to + ``_{PROG_NAME}_COMPLETE``. + + .. versionchanged:: 8.2.0 + Dots (``.``) in ``prog_name`` are replaced with underscores (``_``). + """ + if complete_var is None: + complete_name = prog_name.replace("-", "_").replace(".", "_") + complete_var = f"_{complete_name}_COMPLETE".upper() + + instruction = os.environ.get(complete_var) + + if not instruction: + return + + from .shell_completion import shell_complete + + rv = shell_complete(self, ctx_args, prog_name, complete_var, instruction) + sys.exit(rv) + + def __call__(self, *args: t.Any, **kwargs: t.Any) -> t.Any: + """Alias for :meth:`main`.""" + return self.main(*args, **kwargs) + + +class _FakeSubclassCheck(type): + def __subclasscheck__(cls, subclass: type) -> bool: + return issubclass(subclass, cls.__bases__[0]) + + def __instancecheck__(cls, instance: t.Any) -> bool: + return isinstance(instance, cls.__bases__[0]) + + +class _BaseCommand(Command, metaclass=_FakeSubclassCheck): + """ + .. deprecated:: 8.2 + Will be removed in Click 9.0. Use ``Command`` instead. + """ + + +class Group(Command): + """A group is a command that nests other commands (or more groups). + + :param name: The name of the group command. + :param commands: Map names to :class:`Command` objects. Can be a list, which + will use :attr:`Command.name` as the keys. + :param invoke_without_command: Invoke the group's callback even if a + subcommand is not given. + :param no_args_is_help: If no arguments are given, show the group's help and + exit. Defaults to the opposite of ``invoke_without_command``. + :param subcommand_metavar: How to represent the subcommand argument in help. + The default will represent whether ``chain`` is set or not. + :param chain: Allow passing more than one subcommand argument. After parsing + a command's arguments, if any arguments remain another command will be + matched, and so on. + :param result_callback: A function to call after the group's and + subcommand's callbacks. The value returned by the subcommand is passed. + If ``chain`` is enabled, the value will be a list of values returned by + all the commands. If ``invoke_without_command`` is enabled, the value + will be the value returned by the group's callback, or an empty list if + ``chain`` is enabled. + :param kwargs: Other arguments passed to :class:`Command`. + + .. versionchanged:: 8.0 + The ``commands`` argument can be a list of command objects. + + .. versionchanged:: 8.2 + Merged with and replaces the ``MultiCommand`` base class. + """ + + allow_extra_args = True + allow_interspersed_args = False + + #: If set, this is used by the group's :meth:`command` decorator + #: as the default :class:`Command` class. This is useful to make all + #: subcommands use a custom command class. + #: + #: .. versionadded:: 8.0 + command_class: type[Command] | None = None + + #: If set, this is used by the group's :meth:`group` decorator + #: as the default :class:`Group` class. This is useful to make all + #: subgroups use a custom group class. + #: + #: If set to the special value :class:`type` (literally + #: ``group_class = type``), this group's class will be used as the + #: default class. This makes a custom group class continue to make + #: custom groups. + #: + #: .. versionadded:: 8.0 + group_class: type[Group] | type[type] | None = None + # Literal[type] isn't valid, so use Type[type] + + def __init__( + self, + name: str | None = None, + commands: cabc.MutableMapping[str, Command] + | cabc.Sequence[Command] + | None = None, + invoke_without_command: bool = False, + no_args_is_help: bool | None = None, + subcommand_metavar: str | None = None, + chain: bool = False, + result_callback: t.Callable[..., t.Any] | None = None, + **kwargs: t.Any, + ) -> None: + super().__init__(name, **kwargs) + + if commands is None: + commands = {} + elif isinstance(commands, abc.Sequence): + commands = {c.name: c for c in commands if c.name is not None} + + #: The registered subcommands by their exported names. + self.commands: cabc.MutableMapping[str, Command] = commands + + if no_args_is_help is None: + no_args_is_help = not invoke_without_command + + self.no_args_is_help = no_args_is_help + self.invoke_without_command = invoke_without_command + + if subcommand_metavar is None: + if chain: + subcommand_metavar = "COMMAND1 [ARGS]... [COMMAND2 [ARGS]...]..." + else: + subcommand_metavar = "COMMAND [ARGS]..." + + self.subcommand_metavar = subcommand_metavar + self.chain = chain + # The result callback that is stored. This can be set or + # overridden with the :func:`result_callback` decorator. + self._result_callback = result_callback + + if self.chain: + for param in self.params: + if isinstance(param, Argument) and not param.required: + raise RuntimeError( + "A group in chain mode cannot have optional arguments." + ) + + def to_info_dict(self, ctx: Context) -> dict[str, t.Any]: + info_dict = super().to_info_dict(ctx) + commands = {} + + for name in self.list_commands(ctx): + command = self.get_command(ctx, name) + + if command is None: + continue + + sub_ctx = ctx._make_sub_context(command) + + with sub_ctx.scope(cleanup=False): + commands[name] = command.to_info_dict(sub_ctx) + + info_dict.update(commands=commands, chain=self.chain) + return info_dict + + def add_command(self, cmd: Command, name: str | None = None) -> None: + """Registers another :class:`Command` with this group. If the name + is not provided, the name of the command is used. + """ + name = name or cmd.name + if name is None: + raise TypeError("Command has no name.") + _check_nested_chain(self, name, cmd, register=True) + self.commands[name] = cmd + + @t.overload + def command(self, __func: t.Callable[..., t.Any]) -> Command: ... + + @t.overload + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Command]: ... + + def command( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Command] | Command: + """A shortcut decorator for declaring and attaching a command to + the group. This takes the same arguments as :func:`command` and + immediately registers the created command with this group by + calling :meth:`add_command`. + + To customize the command class used, set the + :attr:`command_class` attribute. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.0 + Added the :attr:`command_class` attribute. + """ + from .decorators import command + + func: t.Callable[..., t.Any] | None = None + + if args and callable(args[0]): + assert len(args) == 1 and not kwargs, ( + "Use 'command(**kwargs)(callable)' to provide arguments." + ) + (func,) = args + args = () + + if self.command_class and kwargs.get("cls") is None: + kwargs["cls"] = self.command_class + + def decorator(f: t.Callable[..., t.Any]) -> Command: + cmd: Command = command(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + if func is not None: + return decorator(func) + + return decorator + + @t.overload + def group(self, __func: t.Callable[..., t.Any]) -> Group: ... + + @t.overload + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Group]: ... + + def group( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], Group] | Group: + """A shortcut decorator for declaring and attaching a group to + the group. This takes the same arguments as :func:`group` and + immediately registers the created group with this group by + calling :meth:`add_command`. + + To customize the group class used, set the :attr:`group_class` + attribute. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.0 + Added the :attr:`group_class` attribute. + """ + from .decorators import group + + func: t.Callable[..., t.Any] | None = None + + if args and callable(args[0]): + assert len(args) == 1 and not kwargs, ( + "Use 'group(**kwargs)(callable)' to provide arguments." + ) + (func,) = args + args = () + + if self.group_class is not None and kwargs.get("cls") is None: + if self.group_class is type: + kwargs["cls"] = type(self) + else: + kwargs["cls"] = self.group_class + + def decorator(f: t.Callable[..., t.Any]) -> Group: + cmd: Group = group(*args, **kwargs)(f) + self.add_command(cmd) + return cmd + + if func is not None: + return decorator(func) + + return decorator + + def result_callback(self, replace: bool = False) -> t.Callable[[F], F]: + """Adds a result callback to the command. By default if a + result callback is already registered this will chain them but + this can be disabled with the `replace` parameter. The result + callback is invoked with the return value of the subcommand + (or the list of return values from all subcommands if chaining + is enabled) as well as the parameters as they would be passed + to the main callback. + + Example:: + + @click.group() + @click.option('-i', '--input', default=23) + def cli(input): + return 42 + + @cli.result_callback() + def process_result(result, input): + return result + input + + :param replace: if set to `True` an already existing result + callback will be removed. + + .. versionchanged:: 8.0 + Renamed from ``resultcallback``. + + .. versionadded:: 3.0 + """ + + def decorator(f: F) -> F: + old_callback = self._result_callback + + if old_callback is None or replace: + self._result_callback = f + return f + + def function(value: t.Any, /, *args: t.Any, **kwargs: t.Any) -> t.Any: + inner = old_callback(value, *args, **kwargs) + return f(inner, *args, **kwargs) + + self._result_callback = rv = update_wrapper(t.cast(F, function), f) + return rv # type: ignore[return-value] + + return decorator + + def get_command(self, ctx: Context, cmd_name: str) -> Command | None: + """Given a context and a command name, this returns a :class:`Command` + object if it exists or returns ``None``. + """ + return self.commands.get(cmd_name) + + def list_commands(self, ctx: Context) -> list[str]: + """Returns a list of subcommand names in the order they should appear.""" + return sorted(self.commands) + + def collect_usage_pieces(self, ctx: Context) -> list[str]: + rv = super().collect_usage_pieces(ctx) + rv.append(self.subcommand_metavar) + return rv + + def format_options(self, ctx: Context, formatter: HelpFormatter) -> None: + super().format_options(ctx, formatter) + self.format_commands(ctx, formatter) + + def format_commands(self, ctx: Context, formatter: HelpFormatter) -> None: + """Extra format methods for multi methods that adds all the commands + after the options. + """ + commands = [] + for subcommand in self.list_commands(ctx): + cmd = self.get_command(ctx, subcommand) + # What is this, the tool lied about a command. Ignore it + if cmd is None: + continue + if cmd.hidden: + continue + + commands.append((subcommand, cmd)) + + # allow for 3 times the default spacing + if len(commands): + limit = formatter.width - 6 - max(len(cmd[0]) for cmd in commands) + + rows = [] + for subcommand, cmd in commands: + help = cmd.get_short_help_str(limit) + rows.append((subcommand, help)) + + if rows: + with formatter.section(_("Commands")): + formatter.write_dl(rows) + + def parse_args(self, ctx: Context, args: list[str]) -> list[str]: + if not args and self.no_args_is_help and not ctx.resilient_parsing: + raise NoArgsIsHelpError(ctx) + + rest = super().parse_args(ctx, args) + + if self.chain: + ctx._protected_args = rest + ctx.args = [] + elif rest: + ctx._protected_args, ctx.args = rest[:1], rest[1:] + + return ctx.args + + def invoke(self, ctx: Context) -> t.Any: + def _process_result(value: t.Any) -> t.Any: + if self._result_callback is not None: + value = ctx.invoke(self._result_callback, value, **ctx.params) + return value + + if not ctx._protected_args: + if self.invoke_without_command: + # No subcommand was invoked, so the result callback is + # invoked with the group return value for regular + # groups, or an empty list for chained groups. + with ctx: + rv = super().invoke(ctx) + return _process_result([] if self.chain else rv) + ctx.fail(_("Missing command.")) + + # Fetch args back out + args = [*ctx._protected_args, *ctx.args] + ctx.args = [] + ctx._protected_args = [] + + # If we're not in chain mode, we only allow the invocation of a + # single command but we also inform the current context about the + # name of the command to invoke. + if not self.chain: + # Make sure the context is entered so we do not clean up + # resources until the result processor has worked. + with ctx: + cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None + ctx.invoked_subcommand = cmd_name + super().invoke(ctx) + sub_ctx = cmd.make_context(cmd_name, args, parent=ctx) + with sub_ctx: + return _process_result(sub_ctx.command.invoke(sub_ctx)) + + # In chain mode we create the contexts step by step, but after the + # base command has been invoked. Because at that point we do not + # know the subcommands yet, the invoked subcommand attribute is + # set to ``*`` to inform the command that subcommands are executed + # but nothing else. + with ctx: + ctx.invoked_subcommand = "*" if args else None + super().invoke(ctx) + + # Otherwise we make every single context and invoke them in a + # chain. In that case the return value to the result processor + # is the list of all invoked subcommand's results. + contexts = [] + while args: + cmd_name, cmd, args = self.resolve_command(ctx, args) + assert cmd is not None + sub_ctx = cmd.make_context( + cmd_name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + ) + contexts.append(sub_ctx) + args, sub_ctx.args = sub_ctx.args, [] + + rv = [] + for sub_ctx in contexts: + with sub_ctx: + rv.append(sub_ctx.command.invoke(sub_ctx)) + return _process_result(rv) + + def resolve_command( + self, ctx: Context, args: list[str] + ) -> tuple[str | None, Command | None, list[str]]: + cmd_name = make_str(args[0]) + original_cmd_name = cmd_name + + # Get the command + cmd = self.get_command(ctx, cmd_name) + + # If we can't find the command but there is a normalization + # function available, we try with that one. + if cmd is None and ctx.token_normalize_func is not None: + cmd_name = ctx.token_normalize_func(cmd_name) + cmd = self.get_command(ctx, cmd_name) + + # If we don't find the command we want to show an error message + # to the user that it was not provided. However, there is + # something else we should do: if the first argument looks like + # an option we want to kick off parsing again for arguments to + # resolve things like --help which now should go to the main + # place. + if cmd is None and not ctx.resilient_parsing: + if _split_opt(cmd_name)[0]: + self.parse_args(ctx, args) + ctx.fail(_("No such command {name!r}.").format(name=original_cmd_name)) + return cmd_name if cmd else None, cmd, args[1:] + + def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: + """Return a list of completions for the incomplete value. Looks + at the names of options, subcommands, and chained + multi-commands. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + results = [ + CompletionItem(name, help=command.get_short_help_str()) + for name, command in _complete_visible_commands(ctx, incomplete) + ] + results.extend(super().shell_complete(ctx, incomplete)) + return results + + +class _MultiCommand(Group, metaclass=_FakeSubclassCheck): + """ + .. deprecated:: 8.2 + Will be removed in Click 9.0. Use ``Group`` instead. + """ + + +class CommandCollection(Group): + """A :class:`Group` that looks up subcommands on other groups. If a command + is not found on this group, each registered source is checked in order. + Parameters on a source are not added to this group, and a source's callback + is not invoked when invoking its commands. In other words, this "flattens" + commands in many groups into this one group. + + :param name: The name of the group command. + :param sources: A list of :class:`Group` objects to look up commands from. + :param kwargs: Other arguments passed to :class:`Group`. + + .. versionchanged:: 8.2 + This is a subclass of ``Group``. Commands are looked up first on this + group, then each of its sources. + """ + + def __init__( + self, + name: str | None = None, + sources: list[Group] | None = None, + **kwargs: t.Any, + ) -> None: + super().__init__(name, **kwargs) + #: The list of registered groups. + self.sources: list[Group] = sources or [] + + def add_source(self, group: Group) -> None: + """Add a group as a source of commands.""" + self.sources.append(group) + + def get_command(self, ctx: Context, cmd_name: str) -> Command | None: + rv = super().get_command(ctx, cmd_name) + + if rv is not None: + return rv + + for source in self.sources: + rv = source.get_command(ctx, cmd_name) + + if rv is not None: + if self.chain: + _check_nested_chain(self, cmd_name, rv) + + return rv + + return None + + def list_commands(self, ctx: Context) -> list[str]: + rv: set[str] = set(super().list_commands(ctx)) + + for source in self.sources: + rv.update(source.list_commands(ctx)) + + return sorted(rv) + + +def _check_iter(value: t.Any) -> cabc.Iterator[t.Any]: + """Check if the value is iterable but not a string. Raises a type + error, or return an iterator over the value. + """ + if isinstance(value, str): + raise TypeError + + return iter(value) + + +class Parameter: + r"""A parameter to a command comes in two versions: they are either + :class:`Option`\s or :class:`Argument`\s. Other subclasses are currently + not supported by design as some of the internals for parsing are + intentionally not finalized. + + Some settings are supported by both options and arguments. + + :param param_decls: the parameter declarations for this option or + argument. This is a list of flags or argument + names. + :param type: the type that should be used. Either a :class:`ParamType` + or a Python type. The latter is converted into the former + automatically if supported. + :param required: controls if this is optional or not. + :param default: the default value if omitted. This can also be a callable, + in which case it's invoked when the default is needed + without any arguments. + :param callback: A function to further process or validate the value + after type conversion. It is called as ``f(ctx, param, value)`` + and must return the value. It is called for all sources, + including prompts. + :param nargs: the number of arguments to match. If not ``1`` the return + value is a tuple instead of single value. The default for + nargs is ``1`` (except if the type is a tuple, then it's + the arity of the tuple). If ``nargs=-1``, all remaining + parameters are collected. + :param metavar: how the value is represented in the help page. + :param expose_value: if this is `True` then the value is passed onwards + to the command callback and stored on the context, + otherwise it's skipped. + :param is_eager: eager values are processed before non eager ones. This + should not be set for arguments or it will inverse the + order of processing. + :param envvar: environment variable(s) that are used to provide a default value for + this parameter. This can be a string or a sequence of strings. If a sequence is + given, only the first non-empty environment variable is used for the parameter. + :param shell_complete: A function that returns custom shell + completions. Used instead of the param's type completion if + given. Takes ``ctx, param, incomplete`` and must return a list + of :class:`~click.shell_completion.CompletionItem` or a list of + strings. + :param deprecated: If ``True`` or non-empty string, issues a message + indicating that the argument is deprecated and highlights + its deprecation in --help. The message can be customized + by using a string as the value. A deprecated parameter + cannot be required, a ValueError will be raised otherwise. + + .. versionchanged:: 8.2.0 + Introduction of ``deprecated``. + + .. versionchanged:: 8.2 + Adding duplicate parameter names to a :class:`~click.core.Command` will + result in a ``UserWarning`` being shown. + + .. versionchanged:: 8.2 + Adding duplicate parameter names to a :class:`~click.core.Command` will + result in a ``UserWarning`` being shown. + + .. versionchanged:: 8.0 + ``process_value`` validates required parameters and bounded + ``nargs``, and invokes the parameter callback before returning + the value. This allows the callback to validate prompts. + ``full_process_value`` is removed. + + .. versionchanged:: 8.0 + ``autocompletion`` is renamed to ``shell_complete`` and has new + semantics described above. The old name is deprecated and will + be removed in 8.1, until then it will be wrapped to match the + new requirements. + + .. versionchanged:: 8.0 + For ``multiple=True, nargs>1``, the default must be a list of + tuples. + + .. versionchanged:: 8.0 + Setting a default is no longer required for ``nargs>1``, it will + default to ``None``. ``multiple=True`` or ``nargs=-1`` will + default to ``()``. + + .. versionchanged:: 7.1 + Empty environment variables are ignored rather than taking the + empty string value. This makes it possible for scripts to clear + variables if they can't unset them. + + .. versionchanged:: 2.0 + Changed signature for parameter callback to also be passed the + parameter. The old callback format will still work, but it will + raise a warning to give you a chance to migrate the code easier. + """ + + param_type_name = "parameter" + + def __init__( + self, + param_decls: cabc.Sequence[str] | None = None, + type: types.ParamType | t.Any | None = None, + required: bool = False, + # XXX The default historically embed two concepts: + # - the declaration of a Parameter object carrying the default (handy to + # arbitrage the default value of coupled Parameters sharing the same + # self.name, like flag options), + # - and the actual value of the default. + # It is confusing and is the source of many issues discussed in: + # https://github.com/pallets/click/pull/3030 + # In the future, we might think of splitting it in two, not unlike + # Option.is_flag and Option.flag_value: we could have something like + # Parameter.is_default and Parameter.default_value. + default: t.Any | t.Callable[[], t.Any] | None = UNSET, + callback: t.Callable[[Context, Parameter, t.Any], t.Any] | None = None, + nargs: int | None = None, + multiple: bool = False, + metavar: str | None = None, + expose_value: bool = True, + is_eager: bool = False, + envvar: str | cabc.Sequence[str] | None = None, + shell_complete: t.Callable[ + [Context, Parameter, str], list[CompletionItem] | list[str] + ] + | None = None, + deprecated: bool | str = False, + ) -> None: + self.name: str | None + self.opts: list[str] + self.secondary_opts: list[str] + self.name, self.opts, self.secondary_opts = self._parse_decls( + param_decls or (), expose_value + ) + self.type: types.ParamType = types.convert_type(type, default) + + # Default nargs to what the type tells us if we have that + # information available. + if nargs is None: + if self.type.is_composite: + nargs = self.type.arity + else: + nargs = 1 + + self.required = required + self.callback = callback + self.nargs = nargs + self.multiple = multiple + self.expose_value = expose_value + self.default = default + self.is_eager = is_eager + self.metavar = metavar + self.envvar = envvar + self._custom_shell_complete = shell_complete + self.deprecated = deprecated + + if __debug__: + if self.type.is_composite and nargs != self.type.arity: + raise ValueError( + f"'nargs' must be {self.type.arity} (or None) for" + f" type {self.type!r}, but it was {nargs}." + ) + + if required and deprecated: + raise ValueError( + f"The {self.param_type_name} '{self.human_readable_name}' " + "is deprecated and still required. A deprecated " + f"{self.param_type_name} cannot be required." + ) + + def to_info_dict(self) -> dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionchanged:: 8.3.0 + Returns ``None`` for the :attr:`default` if it was not set. + + .. versionadded:: 8.0 + """ + return { + "name": self.name, + "param_type_name": self.param_type_name, + "opts": self.opts, + "secondary_opts": self.secondary_opts, + "type": self.type.to_info_dict(), + "required": self.required, + "nargs": self.nargs, + "multiple": self.multiple, + # We explicitly hide the :attr:`UNSET` value to the user, as we choose to + # make it an implementation detail. And because ``to_info_dict`` has been + # designed for documentation purposes, we return ``None`` instead. + "default": self.default if self.default is not UNSET else None, + "envvar": self.envvar, + } + + def __repr__(self) -> str: + return f"<{self.__class__.__name__} {self.name}>" + + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str | None, list[str], list[str]]: + raise NotImplementedError() + + @property + def human_readable_name(self) -> str: + """Returns the human readable name of this parameter. This is the + same as the name for options, but the metavar for arguments. + """ + return self.name # type: ignore + + def make_metavar(self, ctx: Context) -> str: + if self.metavar is not None: + return self.metavar + + metavar = self.type.get_metavar(param=self, ctx=ctx) + + if metavar is None: + metavar = self.type.name.upper() + + if self.nargs != 1: + metavar += "..." + + return metavar + + @t.overload + def get_default( + self, ctx: Context, call: t.Literal[True] = True + ) -> t.Any | None: ... + + @t.overload + def get_default( + self, ctx: Context, call: bool = ... + ) -> t.Any | t.Callable[[], t.Any] | None: ... + + def get_default( + self, ctx: Context, call: bool = True + ) -> t.Any | t.Callable[[], t.Any] | None: + """Get the default for the parameter. Tries + :meth:`Context.lookup_default` first, then the local default. + + :param ctx: Current context. + :param call: If the default is a callable, call it. Disable to + return the callable instead. + + .. versionchanged:: 8.0.2 + Type casting is no longer performed when getting a default. + + .. versionchanged:: 8.0.1 + Type casting can fail in resilient parsing mode. Invalid + defaults will not prevent showing help text. + + .. versionchanged:: 8.0 + Looks at ``ctx.default_map`` first. + + .. versionchanged:: 8.0 + Added the ``call`` parameter. + """ + value = ctx.lookup_default(self.name, call=False) # type: ignore + + if value is UNSET: + value = self.default + + if call and callable(value): + value = value() + + return value + + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: + raise NotImplementedError() + + def consume_value( + self, ctx: Context, opts: cabc.Mapping[str, t.Any] + ) -> tuple[t.Any, ParameterSource]: + """Returns the parameter value produced by the parser. + + If the parser did not produce a value from user input, the value is either + sourced from the environment variable, the default map, or the parameter's + default value. In that order of precedence. + + If no value is found, an internal sentinel value is returned. + + :meta private: + """ + # Collect from the parse the value passed by the user to the CLI. + value = opts.get(self.name, UNSET) # type: ignore + # If the value is set, it means it was sourced from the command line by the + # parser, otherwise it left unset by default. + source = ( + ParameterSource.COMMANDLINE + if value is not UNSET + else ParameterSource.DEFAULT + ) + + if value is UNSET: + envvar_value = self.value_from_envvar(ctx) + if envvar_value is not None: + value = envvar_value + source = ParameterSource.ENVIRONMENT + + if value is UNSET: + default_map_value = ctx.lookup_default(self.name) # type: ignore + if default_map_value is not UNSET: + value = default_map_value + source = ParameterSource.DEFAULT_MAP + + if value is UNSET: + default_value = self.get_default(ctx) + if default_value is not UNSET: + value = default_value + source = ParameterSource.DEFAULT + + return value, source + + def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any: + """Convert and validate a value against the parameter's + :attr:`type`, :attr:`multiple`, and :attr:`nargs`. + """ + if value in (None, UNSET): + if self.multiple or self.nargs == -1: + return () + else: + return value + + def check_iter(value: t.Any) -> cabc.Iterator[t.Any]: + try: + return _check_iter(value) + except TypeError: + # This should only happen when passing in args manually, + # the parser should construct an iterable when parsing + # the command line. + raise BadParameter( + _("Value must be an iterable."), ctx=ctx, param=self + ) from None + + # Define the conversion function based on nargs and type. + + if self.nargs == 1 or self.type.is_composite: + + def convert(value: t.Any) -> t.Any: + return self.type(value, param=self, ctx=ctx) + + elif self.nargs == -1: + + def convert(value: t.Any) -> t.Any: # tuple[t.Any, ...] + return tuple(self.type(x, self, ctx) for x in check_iter(value)) + + else: # nargs > 1 + + def convert(value: t.Any) -> t.Any: # tuple[t.Any, ...] + value = tuple(check_iter(value)) + + if len(value) != self.nargs: + raise BadParameter( + ngettext( + "Takes {nargs} values but 1 was given.", + "Takes {nargs} values but {len} were given.", + len(value), + ).format(nargs=self.nargs, len=len(value)), + ctx=ctx, + param=self, + ) + + return tuple(self.type(x, self, ctx) for x in value) + + if self.multiple: + return tuple(convert(x) for x in check_iter(value)) + + return convert(value) + + def value_is_missing(self, value: t.Any) -> bool: + """A value is considered missing if: + + - it is :attr:`UNSET`, + - or if it is an empty sequence while the parameter is suppose to have + non-single value (i.e. :attr:`nargs` is not ``1`` or :attr:`multiple` is + set). + + :meta private: + """ + if value is UNSET: + return True + + if (self.nargs != 1 or self.multiple) and value == (): + return True + + return False + + def process_value(self, ctx: Context, value: t.Any) -> t.Any: + """Process the value of this parameter: + + 1. Type cast the value using :meth:`type_cast_value`. + 2. Check if the value is missing (see: :meth:`value_is_missing`), and raise + :exc:`MissingParameter` if it is required. + 3. If a :attr:`callback` is set, call it to have the value replaced by the + result of the callback. If the value was not set, the callback receive + ``None``. This keep the legacy behavior as it was before the introduction of + the :attr:`UNSET` sentinel. + + :meta private: + """ + value = self.type_cast_value(ctx, value) + + if self.required and self.value_is_missing(value): + raise MissingParameter(ctx=ctx, param=self) + + if self.callback is not None: + # Legacy case: UNSET is not exposed directly to the callback, but converted + # to None. + if value is UNSET: + value = None + value = self.callback(ctx, self, value) + + return value + + def resolve_envvar_value(self, ctx: Context) -> str | None: + """Returns the value found in the environment variable(s) attached to this + parameter. + + Environment variables values are `always returned as strings + `_. + + This method returns ``None`` if: + + - the :attr:`envvar` property is not set on the :class:`Parameter`, + - the environment variable is not found in the environment, + - the variable is found in the environment but its value is empty (i.e. the + environment variable is present but has an empty string). + + If :attr:`envvar` is setup with multiple environment variables, + then only the first non-empty value is returned. + + .. caution:: + + The raw value extracted from the environment is not normalized and is + returned as-is. Any normalization or reconciliation is performed later by + the :class:`Parameter`'s :attr:`type`. + + :meta private: + """ + if not self.envvar: + return None + + if isinstance(self.envvar, str): + rv = os.environ.get(self.envvar) + + if rv: + return rv + else: + for envvar in self.envvar: + rv = os.environ.get(envvar) + + # Return the first non-empty value of the list of environment variables. + if rv: + return rv + # Else, absence of value is interpreted as an environment variable that + # is not set, so proceed to the next one. + + return None + + def value_from_envvar(self, ctx: Context) -> str | cabc.Sequence[str] | None: + """Process the raw environment variable string for this parameter. + + Returns the string as-is or splits it into a sequence of strings if the + parameter is expecting multiple values (i.e. its :attr:`nargs` property is set + to a value other than ``1``). + + :meta private: + """ + rv = self.resolve_envvar_value(ctx) + + if rv is not None and self.nargs != 1: + return self.type.split_envvar_value(rv) + + return rv + + def handle_parse_result( + self, ctx: Context, opts: cabc.Mapping[str, t.Any], args: list[str] + ) -> tuple[t.Any, list[str]]: + """Process the value produced by the parser from user input. + + Always process the value through the Parameter's :attr:`type`, wherever it + comes from. + + If the parameter is deprecated, this method warn the user about it. But only if + the value has been explicitly set by the user (and as such, is not coming from + a default). + + :meta private: + """ + with augment_usage_errors(ctx, param=self): + value, source = self.consume_value(ctx, opts) + + ctx.set_parameter_source(self.name, source) # type: ignore + + # Display a deprecation warning if necessary. + if ( + self.deprecated + and value is not UNSET + and source not in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + ): + extra_message = ( + f" {self.deprecated}" if isinstance(self.deprecated, str) else "" + ) + message = _( + "DeprecationWarning: The {param_type} {name!r} is deprecated." + "{extra_message}" + ).format( + param_type=self.param_type_name, + name=self.human_readable_name, + extra_message=extra_message, + ) + echo(style(message, fg="red"), err=True) + + # Process the value through the parameter's type. + try: + value = self.process_value(ctx, value) + except Exception: + if not ctx.resilient_parsing: + raise + # In resilient parsing mode, we do not want to fail the command if the + # value is incompatible with the parameter type, so we reset the value + # to UNSET, which will be interpreted as a missing value. + value = UNSET + + # Add parameter's value to the context. + if ( + self.expose_value + # We skip adding the value if it was previously set by another parameter + # targeting the same variable name. This prevents parameters competing for + # the same name to override each other. + and self.name not in ctx.params + ): + # Click is logically enforcing that the name is None if the parameter is + # not to be exposed. We still assert it here to please the type checker. + assert self.name is not None, ( + f"{self!r} parameter's name should not be None when exposing value." + ) + # Normalize UNSET values to None, as we're about to pass them to the + # command function and move them to the pure-Python realm of user-written + # code. + ctx.params[self.name] = value if value is not UNSET else None + + return value, args + + def get_help_record(self, ctx: Context) -> tuple[str, str] | None: + pass + + def get_usage_pieces(self, ctx: Context) -> list[str]: + return [] + + def get_error_hint(self, ctx: Context) -> str: + """Get a stringified version of the param for use in error messages to + indicate which param caused the error. + """ + hint_list = self.opts or [self.human_readable_name] + return " / ".join(f"'{x}'" for x in hint_list) + + def shell_complete(self, ctx: Context, incomplete: str) -> list[CompletionItem]: + """Return a list of completions for the incomplete value. If a + ``shell_complete`` function was given during init, it is used. + Otherwise, the :attr:`type` + :meth:`~click.types.ParamType.shell_complete` function is used. + + :param ctx: Invocation context for this command. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + if self._custom_shell_complete is not None: + results = self._custom_shell_complete(ctx, self, incomplete) + + if results and isinstance(results[0], str): + from click.shell_completion import CompletionItem + + results = [CompletionItem(c) for c in results] + + return t.cast("list[CompletionItem]", results) + + return self.type.shell_complete(ctx, self, incomplete) + + +class Option(Parameter): + """Options are usually optional values on the command line and + have some extra features that arguments don't have. + + All other parameters are passed onwards to the parameter constructor. + + :param show_default: Show the default value for this option in its + help text. Values are not shown by default, unless + :attr:`Context.show_default` is ``True``. If this value is a + string, it shows that string in parentheses instead of the + actual value. This is particularly useful for dynamic options. + For single option boolean flags, the default remains hidden if + its value is ``False``. + :param show_envvar: Controls if an environment variable should be + shown on the help page and error messages. + Normally, environment variables are not shown. + :param prompt: If set to ``True`` or a non empty string then the + user will be prompted for input. If set to ``True`` the prompt + will be the option name capitalized. A deprecated option cannot be + prompted. + :param confirmation_prompt: Prompt a second time to confirm the + value if it was prompted for. Can be set to a string instead of + ``True`` to customize the message. + :param prompt_required: If set to ``False``, the user will be + prompted for input only when the option was specified as a flag + without a value. + :param hide_input: If this is ``True`` then the input on the prompt + will be hidden from the user. This is useful for password input. + :param is_flag: forces this option to act as a flag. The default is + auto detection. + :param flag_value: which value should be used for this flag if it's + enabled. This is set to a boolean automatically if + the option string contains a slash to mark two options. + :param multiple: if this is set to `True` then the argument is accepted + multiple times and recorded. This is similar to ``nargs`` + in how it works but supports arbitrary number of + arguments. + :param count: this flag makes an option increment an integer. + :param allow_from_autoenv: if this is enabled then the value of this + parameter will be pulled from an environment + variable in case a prefix is defined on the + context. + :param help: the help string. + :param hidden: hide this option from help outputs. + :param attrs: Other command arguments described in :class:`Parameter`. + + .. versionchanged:: 8.2 + ``envvar`` used with ``flag_value`` will always use the ``flag_value``, + previously it would use the value of the environment variable. + + .. versionchanged:: 8.1 + Help text indentation is cleaned here instead of only in the + ``@option`` decorator. + + .. versionchanged:: 8.1 + The ``show_default`` parameter overrides + ``Context.show_default``. + + .. versionchanged:: 8.1 + The default of a single option boolean flag is not shown if the + default value is ``False``. + + .. versionchanged:: 8.0.1 + ``type`` is detected from ``flag_value`` if given. + """ + + param_type_name = "option" + + def __init__( + self, + param_decls: cabc.Sequence[str] | None = None, + show_default: bool | str | None = None, + prompt: bool | str = False, + confirmation_prompt: bool | str = False, + prompt_required: bool = True, + hide_input: bool = False, + is_flag: bool | None = None, + flag_value: t.Any = UNSET, + multiple: bool = False, + count: bool = False, + allow_from_autoenv: bool = True, + type: types.ParamType | t.Any | None = None, + help: str | None = None, + hidden: bool = False, + show_choices: bool = True, + show_envvar: bool = False, + deprecated: bool | str = False, + **attrs: t.Any, + ) -> None: + if help: + help = inspect.cleandoc(help) + + super().__init__( + param_decls, type=type, multiple=multiple, deprecated=deprecated, **attrs + ) + + if prompt is True: + if self.name is None: + raise TypeError("'name' is required with 'prompt=True'.") + + prompt_text: str | None = self.name.replace("_", " ").capitalize() + elif prompt is False: + prompt_text = None + else: + prompt_text = prompt + + if deprecated: + deprecated_message = ( + f"(DEPRECATED: {deprecated})" + if isinstance(deprecated, str) + else "(DEPRECATED)" + ) + help = help + deprecated_message if help is not None else deprecated_message + + self.prompt = prompt_text + self.confirmation_prompt = confirmation_prompt + self.prompt_required = prompt_required + self.hide_input = hide_input + self.hidden = hidden + + # The _flag_needs_value property tells the parser that this option is a flag + # that cannot be used standalone and needs a value. With this information, the + # parser can determine whether to consider the next user-provided argument in + # the CLI as a value for this flag or as a new option. + # If prompt is enabled but not required, then it opens the possibility for the + # option to gets its value from the user. + self._flag_needs_value = self.prompt is not None and not self.prompt_required + + # Auto-detect if this is a flag or not. + if is_flag is None: + # Implicitly a flag because flag_value was set. + if flag_value is not UNSET: + is_flag = True + # Not a flag, but when used as a flag it shows a prompt. + elif self._flag_needs_value: + is_flag = False + # Implicitly a flag because secondary options names were given. + elif self.secondary_opts: + is_flag = True + # The option is explicitly not a flag. But we do not know yet if it needs a + # value or not. So we look at the default value to determine it. + elif is_flag is False and not self._flag_needs_value: + self._flag_needs_value = self.default is UNSET + + if is_flag: + # Set missing default for flags if not explicitly required or prompted. + if self.default is UNSET and not self.required and not self.prompt: + if multiple: + self.default = () + + # Auto-detect the type of the flag based on the flag_value. + if type is None: + # A flag without a flag_value is a boolean flag. + if flag_value is UNSET: + self.type = types.BoolParamType() + # If the flag value is a boolean, use BoolParamType. + elif isinstance(flag_value, bool): + self.type = types.BoolParamType() + # Otherwise, guess the type from the flag value. + else: + self.type = types.convert_type(None, flag_value) + + self.is_flag: bool = bool(is_flag) + self.is_bool_flag: bool = bool( + is_flag and isinstance(self.type, types.BoolParamType) + ) + self.flag_value: t.Any = flag_value + + # Set boolean flag default to False if unset and not required. + if self.is_bool_flag: + if self.default is UNSET and not self.required: + self.default = False + + # Support the special case of aligning the default value with the flag_value + # for flags whose default is explicitly set to True. Note that as long as we + # have this condition, there is no way a flag can have a default set to True, + # and a flag_value set to something else. Refs: + # https://github.com/pallets/click/issues/3024#issuecomment-3146199461 + # https://github.com/pallets/click/pull/3030/commits/06847da + if self.default is True and self.flag_value is not UNSET: + self.default = self.flag_value + + # Set the default flag_value if it is not set. + if self.flag_value is UNSET: + if self.is_flag: + self.flag_value = True + else: + self.flag_value = None + + # Counting. + self.count = count + if count: + if type is None: + self.type = types.IntRange(min=0) + if self.default is UNSET: + self.default = 0 + + self.allow_from_autoenv = allow_from_autoenv + self.help = help + self.show_default = show_default + self.show_choices = show_choices + self.show_envvar = show_envvar + + if __debug__: + if deprecated and prompt: + raise ValueError("`deprecated` options cannot use `prompt`.") + + if self.nargs == -1: + raise TypeError("nargs=-1 is not supported for options.") + + if not self.is_bool_flag and self.secondary_opts: + raise TypeError("Secondary flag is not valid for non-boolean flag.") + + if self.is_bool_flag and self.hide_input and self.prompt is not None: + raise TypeError( + "'prompt' with 'hide_input' is not valid for boolean flag." + ) + + if self.count: + if self.multiple: + raise TypeError("'count' is not valid with 'multiple'.") + + if self.is_flag: + raise TypeError("'count' is not valid with 'is_flag'.") + + def to_info_dict(self) -> dict[str, t.Any]: + """ + .. versionchanged:: 8.3.0 + Returns ``None`` for the :attr:`flag_value` if it was not set. + """ + info_dict = super().to_info_dict() + info_dict.update( + help=self.help, + prompt=self.prompt, + is_flag=self.is_flag, + # We explicitly hide the :attr:`UNSET` value to the user, as we choose to + # make it an implementation detail. And because ``to_info_dict`` has been + # designed for documentation purposes, we return ``None`` instead. + flag_value=self.flag_value if self.flag_value is not UNSET else None, + count=self.count, + hidden=self.hidden, + ) + return info_dict + + def get_error_hint(self, ctx: Context) -> str: + result = super().get_error_hint(ctx) + if self.show_envvar and self.envvar is not None: + result += f" (env var: '{self.envvar}')" + return result + + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str | None, list[str], list[str]]: + opts = [] + secondary_opts = [] + name = None + possible_names = [] + + for decl in decls: + if decl.isidentifier(): + if name is not None: + raise TypeError(f"Name '{name}' defined twice") + name = decl + else: + split_char = ";" if decl[:1] == "/" else "/" + if split_char in decl: + first, second = decl.split(split_char, 1) + first = first.rstrip() + if first: + possible_names.append(_split_opt(first)) + opts.append(first) + second = second.lstrip() + if second: + secondary_opts.append(second.lstrip()) + if first == second: + raise ValueError( + f"Boolean option {decl!r} cannot use the" + " same flag for true/false." + ) + else: + possible_names.append(_split_opt(decl)) + opts.append(decl) + + if name is None and possible_names: + possible_names.sort(key=lambda x: -len(x[0])) # group long options first + name = possible_names[0][1].replace("-", "_").lower() + if not name.isidentifier(): + name = None + + if name is None: + if not expose_value: + return None, opts, secondary_opts + raise TypeError( + f"Could not determine name for option with declarations {decls!r}" + ) + + if not opts and not secondary_opts: + raise TypeError( + f"No options defined but a name was passed ({name})." + " Did you mean to declare an argument instead? Did" + f" you mean to pass '--{name}'?" + ) + + return name, opts, secondary_opts + + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: + if self.multiple: + action = "append" + elif self.count: + action = "count" + else: + action = "store" + + if self.is_flag: + action = f"{action}_const" + + if self.is_bool_flag and self.secondary_opts: + parser.add_option( + obj=self, opts=self.opts, dest=self.name, action=action, const=True + ) + parser.add_option( + obj=self, + opts=self.secondary_opts, + dest=self.name, + action=action, + const=False, + ) + else: + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + const=self.flag_value, + ) + else: + parser.add_option( + obj=self, + opts=self.opts, + dest=self.name, + action=action, + nargs=self.nargs, + ) + + def get_help_record(self, ctx: Context) -> tuple[str, str] | None: + if self.hidden: + return None + + any_prefix_is_slash = False + + def _write_opts(opts: cabc.Sequence[str]) -> str: + nonlocal any_prefix_is_slash + + rv, any_slashes = join_options(opts) + + if any_slashes: + any_prefix_is_slash = True + + if not self.is_flag and not self.count: + rv += f" {self.make_metavar(ctx=ctx)}" + + return rv + + rv = [_write_opts(self.opts)] + + if self.secondary_opts: + rv.append(_write_opts(self.secondary_opts)) + + help = self.help or "" + + extra = self.get_help_extra(ctx) + extra_items = [] + if "envvars" in extra: + extra_items.append( + _("env var: {var}").format(var=", ".join(extra["envvars"])) + ) + if "default" in extra: + extra_items.append(_("default: {default}").format(default=extra["default"])) + if "range" in extra: + extra_items.append(extra["range"]) + if "required" in extra: + extra_items.append(_(extra["required"])) + + if extra_items: + extra_str = "; ".join(extra_items) + help = f"{help} [{extra_str}]" if help else f"[{extra_str}]" + + return ("; " if any_prefix_is_slash else " / ").join(rv), help + + def get_help_extra(self, ctx: Context) -> types.OptionHelpExtra: + extra: types.OptionHelpExtra = {} + + if self.show_envvar: + envvar = self.envvar + + if envvar is None: + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name is not None + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + + if envvar is not None: + if isinstance(envvar, str): + extra["envvars"] = (envvar,) + else: + extra["envvars"] = tuple(str(d) for d in envvar) + + # Temporarily enable resilient parsing to avoid type casting + # failing for the default. Might be possible to extend this to + # help formatting in general. + resilient = ctx.resilient_parsing + ctx.resilient_parsing = True + + try: + default_value = self.get_default(ctx, call=False) + finally: + ctx.resilient_parsing = resilient + + show_default = False + show_default_is_str = False + + if self.show_default is not None: + if isinstance(self.show_default, str): + show_default_is_str = show_default = True + else: + show_default = self.show_default + elif ctx.show_default is not None: + show_default = ctx.show_default + + if show_default_is_str or ( + show_default and (default_value not in (None, UNSET)) + ): + if show_default_is_str: + default_string = f"({self.show_default})" + elif isinstance(default_value, (list, tuple)): + default_string = ", ".join(str(d) for d in default_value) + elif isinstance(default_value, enum.Enum): + default_string = default_value.name + elif inspect.isfunction(default_value): + default_string = _("(dynamic)") + elif self.is_bool_flag and self.secondary_opts: + # For boolean flags that have distinct True/False opts, + # use the opt without prefix instead of the value. + default_string = _split_opt( + (self.opts if default_value else self.secondary_opts)[0] + )[1] + elif self.is_bool_flag and not self.secondary_opts and not default_value: + default_string = "" + elif default_value == "": + default_string = '""' + else: + default_string = str(default_value) + + if default_string: + extra["default"] = default_string + + if ( + isinstance(self.type, types._NumberRangeBase) + # skip count with default range type + and not (self.count and self.type.min == 0 and self.type.max is None) + ): + range_str = self.type._describe_range() + + if range_str: + extra["range"] = range_str + + if self.required: + extra["required"] = "required" + + return extra + + def prompt_for_value(self, ctx: Context) -> t.Any: + """This is an alternative flow that can be activated in the full + value processing if a value does not exist. It will prompt the + user until a valid value exists and then returns the processed + value as result. + """ + assert self.prompt is not None + + # Calculate the default before prompting anything to lock in the value before + # attempting any user interaction. + default = self.get_default(ctx) + + # A boolean flag can use a simplified [y/n] confirmation prompt. + if self.is_bool_flag: + # If we have no boolean default, we force the user to explicitly provide + # one. + if default in (UNSET, None): + default = None + # Nothing prevent you to declare an option that is simultaneously: + # 1) auto-detected as a boolean flag, + # 2) allowed to prompt, and + # 3) still declare a non-boolean default. + # This forced casting into a boolean is necessary to align any non-boolean + # default to the prompt, which is going to be a [y/n]-style confirmation + # because the option is still a boolean flag. That way, instead of [y/n], + # we get [Y/n] or [y/N] depending on the truthy value of the default. + # Refs: https://github.com/pallets/click/pull/3030#discussion_r2289180249 + else: + default = bool(default) + return confirm(self.prompt, default) + + # If show_default is set to True/False, provide this to `prompt` as well. For + # non-bool values of `show_default`, we use `prompt`'s default behavior + prompt_kwargs: t.Any = {} + if isinstance(self.show_default, bool): + prompt_kwargs["show_default"] = self.show_default + + return prompt( + self.prompt, + # Use ``None`` to inform the prompt() function to reiterate until a valid + # value is provided by the user if we have no default. + default=None if default is UNSET else default, + type=self.type, + hide_input=self.hide_input, + show_choices=self.show_choices, + confirmation_prompt=self.confirmation_prompt, + value_proc=lambda x: self.process_value(ctx, x), + **prompt_kwargs, + ) + + def resolve_envvar_value(self, ctx: Context) -> str | None: + """:class:`Option` resolves its environment variable the same way as + :func:`Parameter.resolve_envvar_value`, but it also supports + :attr:`Context.auto_envvar_prefix`. If we could not find an environment from + the :attr:`envvar` property, we fallback on :attr:`Context.auto_envvar_prefix` + to build dynamiccaly the environment variable name using the + :python:`{ctx.auto_envvar_prefix}_{self.name.upper()}` template. + + :meta private: + """ + rv = super().resolve_envvar_value(ctx) + + if rv is not None: + return rv + + if ( + self.allow_from_autoenv + and ctx.auto_envvar_prefix is not None + and self.name is not None + ): + envvar = f"{ctx.auto_envvar_prefix}_{self.name.upper()}" + rv = os.environ.get(envvar) + + if rv: + return rv + + return None + + def value_from_envvar(self, ctx: Context) -> t.Any: + """For :class:`Option`, this method processes the raw environment variable + string the same way as :func:`Parameter.value_from_envvar` does. + + But in the case of non-boolean flags, the value is analyzed to determine if the + flag is activated or not, and returns a boolean of its activation, or the + :attr:`flag_value` if the latter is set. + + This method also takes care of repeated options (i.e. options with + :attr:`multiple` set to ``True``). + + :meta private: + """ + rv = self.resolve_envvar_value(ctx) + + # Absent environment variable or an empty string is interpreted as unset. + if rv is None: + return None + + # Non-boolean flags are more liberal in what they accept. But a flag being a + # flag, its envvar value still needs to be analyzed to determine if the flag is + # activated or not. + if self.is_flag and not self.is_bool_flag: + # If the flag_value is set and match the envvar value, return it + # directly. + if self.flag_value is not UNSET and rv == self.flag_value: + return self.flag_value + # Analyze the envvar value as a boolean to know if the flag is + # activated or not. + return types.BoolParamType.str_to_bool(rv) + + # Split the envvar value if it is allowed to be repeated. + value_depth = (self.nargs != 1) + bool(self.multiple) + if value_depth > 0: + multi_rv = self.type.split_envvar_value(rv) + if self.multiple and self.nargs != 1: + multi_rv = batch(multi_rv, self.nargs) # type: ignore[assignment] + + return multi_rv + + return rv + + def consume_value( + self, ctx: Context, opts: cabc.Mapping[str, Parameter] + ) -> tuple[t.Any, ParameterSource]: + """For :class:`Option`, the value can be collected from an interactive prompt + if the option is a flag that needs a value (and the :attr:`prompt` property is + set). + + Additionally, this method handles flag option that are activated without a + value, in which case the :attr:`flag_value` is returned. + + :meta private: + """ + value, source = super().consume_value(ctx, opts) + + # The parser will emit a sentinel value if the option is allowed to as a flag + # without a value. + if value is FLAG_NEEDS_VALUE: + # If the option allows for a prompt, we start an interaction with the user. + if self.prompt is not None and not ctx.resilient_parsing: + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + # Else the flag takes its flag_value as value. + else: + value = self.flag_value + source = ParameterSource.COMMANDLINE + + # A flag which is activated always returns the flag value, unless the value + # comes from the explicitly sets default. + elif ( + self.is_flag + and value is True + and not self.is_bool_flag + and source not in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + ): + value = self.flag_value + + # Re-interpret a multiple option which has been sent as-is by the parser. + # Here we replace each occurrence of value-less flags (marked by the + # FLAG_NEEDS_VALUE sentinel) with the flag_value. + elif ( + self.multiple + and value is not UNSET + and source not in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + and any(v is FLAG_NEEDS_VALUE for v in value) + ): + value = [self.flag_value if v is FLAG_NEEDS_VALUE else v for v in value] + source = ParameterSource.COMMANDLINE + + # The value wasn't set, or used the param's default, prompt for one to the user + # if prompting is enabled. + elif ( + ( + value is UNSET + or source in (ParameterSource.DEFAULT, ParameterSource.DEFAULT_MAP) + ) + and self.prompt is not None + and (self.required or self.prompt_required) + and not ctx.resilient_parsing + ): + value = self.prompt_for_value(ctx) + source = ParameterSource.PROMPT + + return value, source + + def type_cast_value(self, ctx: Context, value: t.Any) -> t.Any: + if self.is_flag and not self.required: + if value is UNSET: + if self.is_bool_flag: + # If the flag is a boolean flag, we return False if it is not set. + value = False + return super().type_cast_value(ctx, value) + + +class Argument(Parameter): + """Arguments are positional parameters to a command. They generally + provide fewer features than options but can have infinite ``nargs`` + and are required by default. + + All parameters are passed onwards to the constructor of :class:`Parameter`. + """ + + param_type_name = "argument" + + def __init__( + self, + param_decls: cabc.Sequence[str], + required: bool | None = None, + **attrs: t.Any, + ) -> None: + # Auto-detect the requirement status of the argument if not explicitly set. + if required is None: + # The argument gets automatically required if it has no explicit default + # value set and is setup to match at least one value. + if attrs.get("default", UNSET) is UNSET: + required = attrs.get("nargs", 1) > 0 + # If the argument has a default value, it is not required. + else: + required = False + + if "multiple" in attrs: + raise TypeError("__init__() got an unexpected keyword argument 'multiple'.") + + super().__init__(param_decls, required=required, **attrs) + + @property + def human_readable_name(self) -> str: + if self.metavar is not None: + return self.metavar + return self.name.upper() # type: ignore + + def make_metavar(self, ctx: Context) -> str: + if self.metavar is not None: + return self.metavar + var = self.type.get_metavar(param=self, ctx=ctx) + if not var: + var = self.name.upper() # type: ignore + if self.deprecated: + var += "!" + if not self.required: + var = f"[{var}]" + if self.nargs != 1: + var += "..." + return var + + def _parse_decls( + self, decls: cabc.Sequence[str], expose_value: bool + ) -> tuple[str | None, list[str], list[str]]: + if not decls: + if not expose_value: + return None, [], [] + raise TypeError("Argument is marked as exposed, but does not have a name.") + if len(decls) == 1: + name = arg = decls[0] + name = name.replace("-", "_").lower() + else: + raise TypeError( + "Arguments take exactly one parameter declaration, got" + f" {len(decls)}: {decls}." + ) + return name, [arg], [] + + def get_usage_pieces(self, ctx: Context) -> list[str]: + return [self.make_metavar(ctx)] + + def get_error_hint(self, ctx: Context) -> str: + return f"'{self.make_metavar(ctx)}'" + + def add_to_parser(self, parser: _OptionParser, ctx: Context) -> None: + parser.add_argument(dest=self.name, nargs=self.nargs, obj=self) + + +def __getattr__(name: str) -> object: + import warnings + + if name == "BaseCommand": + warnings.warn( + "'BaseCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Command' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _BaseCommand + + if name == "MultiCommand": + warnings.warn( + "'MultiCommand' is deprecated and will be removed in Click 9.0. Use" + " 'Group' instead.", + DeprecationWarning, + stacklevel=2, + ) + return _MultiCommand + + raise AttributeError(name) diff --git a/netdeploy/lib/python3.11/site-packages/click/decorators.py b/netdeploy/lib/python3.11/site-packages/click/decorators.py new file mode 100644 index 0000000..21f4c34 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/decorators.py @@ -0,0 +1,551 @@ +from __future__ import annotations + +import inspect +import typing as t +from functools import update_wrapper +from gettext import gettext as _ + +from .core import Argument +from .core import Command +from .core import Context +from .core import Group +from .core import Option +from .core import Parameter +from .globals import get_current_context +from .utils import echo + +if t.TYPE_CHECKING: + import typing_extensions as te + + P = te.ParamSpec("P") + +R = t.TypeVar("R") +T = t.TypeVar("T") +_AnyCallable = t.Callable[..., t.Any] +FC = t.TypeVar("FC", bound="_AnyCallable | Command") + + +def pass_context(f: t.Callable[te.Concatenate[Context, P], R]) -> t.Callable[P, R]: + """Marks a callback as wanting to receive the current context + object as first argument. + """ + + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + return f(get_current_context(), *args, **kwargs) + + return update_wrapper(new_func, f) + + +def pass_obj(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]: + """Similar to :func:`pass_context`, but only pass the object on the + context onwards (:attr:`Context.obj`). This is useful if that object + represents the state of a nested system. + """ + + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + return f(get_current_context().obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + +def make_pass_decorator( + object_type: type[T], ensure: bool = False +) -> t.Callable[[t.Callable[te.Concatenate[T, P], R]], t.Callable[P, R]]: + """Given an object type this creates a decorator that will work + similar to :func:`pass_obj` but instead of passing the object of the + current context, it will find the innermost context of type + :func:`object_type`. + + This generates a decorator that works roughly like this:: + + from functools import update_wrapper + + def decorator(f): + @pass_context + def new_func(ctx, *args, **kwargs): + obj = ctx.find_object(object_type) + return ctx.invoke(f, obj, *args, **kwargs) + return update_wrapper(new_func, f) + return decorator + + :param object_type: the type of the object to pass. + :param ensure: if set to `True`, a new object will be created and + remembered on the context if it's not there yet. + """ + + def decorator(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]: + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + ctx = get_current_context() + + obj: T | None + if ensure: + obj = ctx.ensure_object(object_type) + else: + obj = ctx.find_object(object_type) + + if obj is None: + raise RuntimeError( + "Managed to invoke callback without a context" + f" object of type {object_type.__name__!r}" + " existing." + ) + + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + return decorator + + +def pass_meta_key( + key: str, *, doc_description: str | None = None +) -> t.Callable[[t.Callable[te.Concatenate[T, P], R]], t.Callable[P, R]]: + """Create a decorator that passes a key from + :attr:`click.Context.meta` as the first argument to the decorated + function. + + :param key: Key in ``Context.meta`` to pass. + :param doc_description: Description of the object being passed, + inserted into the decorator's docstring. Defaults to "the 'key' + key from Context.meta". + + .. versionadded:: 8.0 + """ + + def decorator(f: t.Callable[te.Concatenate[T, P], R]) -> t.Callable[P, R]: + def new_func(*args: P.args, **kwargs: P.kwargs) -> R: + ctx = get_current_context() + obj = ctx.meta[key] + return ctx.invoke(f, obj, *args, **kwargs) + + return update_wrapper(new_func, f) + + if doc_description is None: + doc_description = f"the {key!r} key from :attr:`click.Context.meta`" + + decorator.__doc__ = ( + f"Decorator that passes {doc_description} as the first argument" + " to the decorated function." + ) + return decorator + + +CmdType = t.TypeVar("CmdType", bound=Command) + + +# variant: no call, directly as decorator for a function. +@t.overload +def command(name: _AnyCallable) -> Command: ... + + +# variant: with positional name and with positional or keyword cls argument: +# @command(namearg, CommandCls, ...) or @command(namearg, cls=CommandCls, ...) +@t.overload +def command( + name: str | None, + cls: type[CmdType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], CmdType]: ... + + +# variant: name omitted, cls _must_ be a keyword argument, @command(cls=CommandCls, ...) +@t.overload +def command( + name: None = None, + *, + cls: type[CmdType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], CmdType]: ... + + +# variant: with optional string name, no cls argument provided. +@t.overload +def command( + name: str | None = ..., cls: None = None, **attrs: t.Any +) -> t.Callable[[_AnyCallable], Command]: ... + + +def command( + name: str | _AnyCallable | None = None, + cls: type[CmdType] | None = None, + **attrs: t.Any, +) -> Command | t.Callable[[_AnyCallable], Command | CmdType]: + r"""Creates a new :class:`Command` and uses the decorated function as + callback. This will also automatically attach all decorated + :func:`option`\s and :func:`argument`\s as parameters to the command. + + The name of the command defaults to the name of the function, converted to + lowercase, with underscores ``_`` replaced by dashes ``-``, and the suffixes + ``_command``, ``_cmd``, ``_group``, and ``_grp`` are removed. For example, + ``init_data_command`` becomes ``init-data``. + + All keyword arguments are forwarded to the underlying command class. + For the ``params`` argument, any decorated params are appended to + the end of the list. + + Once decorated the function turns into a :class:`Command` instance + that can be invoked as a command line utility or be attached to a + command :class:`Group`. + + :param name: The name of the command. Defaults to modifying the function's + name as described above. + :param cls: The command class to create. Defaults to :class:`Command`. + + .. versionchanged:: 8.2 + The suffixes ``_command``, ``_cmd``, ``_group``, and ``_grp`` are + removed when generating the name. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + + .. versionchanged:: 8.1 + The ``params`` argument can be used. Decorated params are + appended to the end of the list. + """ + + func: t.Callable[[_AnyCallable], t.Any] | None = None + + if callable(name): + func = name + name = None + assert cls is None, "Use 'command(cls=cls)(callable)' to specify a class." + assert not attrs, "Use 'command(**kwargs)(callable)' to provide arguments." + + if cls is None: + cls = t.cast("type[CmdType]", Command) + + def decorator(f: _AnyCallable) -> CmdType: + if isinstance(f, Command): + raise TypeError("Attempted to convert a callback into a command twice.") + + attr_params = attrs.pop("params", None) + params = attr_params if attr_params is not None else [] + + try: + decorator_params = f.__click_params__ # type: ignore + except AttributeError: + pass + else: + del f.__click_params__ # type: ignore + params.extend(reversed(decorator_params)) + + if attrs.get("help") is None: + attrs["help"] = f.__doc__ + + if t.TYPE_CHECKING: + assert cls is not None + assert not callable(name) + + if name is not None: + cmd_name = name + else: + cmd_name = f.__name__.lower().replace("_", "-") + cmd_left, sep, suffix = cmd_name.rpartition("-") + + if sep and suffix in {"command", "cmd", "group", "grp"}: + cmd_name = cmd_left + + cmd = cls(name=cmd_name, callback=f, params=params, **attrs) + cmd.__doc__ = f.__doc__ + return cmd + + if func is not None: + return decorator(func) + + return decorator + + +GrpType = t.TypeVar("GrpType", bound=Group) + + +# variant: no call, directly as decorator for a function. +@t.overload +def group(name: _AnyCallable) -> Group: ... + + +# variant: with positional name and with positional or keyword cls argument: +# @group(namearg, GroupCls, ...) or @group(namearg, cls=GroupCls, ...) +@t.overload +def group( + name: str | None, + cls: type[GrpType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], GrpType]: ... + + +# variant: name omitted, cls _must_ be a keyword argument, @group(cmd=GroupCls, ...) +@t.overload +def group( + name: None = None, + *, + cls: type[GrpType], + **attrs: t.Any, +) -> t.Callable[[_AnyCallable], GrpType]: ... + + +# variant: with optional string name, no cls argument provided. +@t.overload +def group( + name: str | None = ..., cls: None = None, **attrs: t.Any +) -> t.Callable[[_AnyCallable], Group]: ... + + +def group( + name: str | _AnyCallable | None = None, + cls: type[GrpType] | None = None, + **attrs: t.Any, +) -> Group | t.Callable[[_AnyCallable], Group | GrpType]: + """Creates a new :class:`Group` with a function as callback. This + works otherwise the same as :func:`command` just that the `cls` + parameter is set to :class:`Group`. + + .. versionchanged:: 8.1 + This decorator can be applied without parentheses. + """ + if cls is None: + cls = t.cast("type[GrpType]", Group) + + if callable(name): + return command(cls=cls, **attrs)(name) + + return command(name, cls, **attrs) + + +def _param_memo(f: t.Callable[..., t.Any], param: Parameter) -> None: + if isinstance(f, Command): + f.params.append(param) + else: + if not hasattr(f, "__click_params__"): + f.__click_params__ = [] # type: ignore + + f.__click_params__.append(param) # type: ignore + + +def argument( + *param_decls: str, cls: type[Argument] | None = None, **attrs: t.Any +) -> t.Callable[[FC], FC]: + """Attaches an argument to the command. All positional arguments are + passed as parameter declarations to :class:`Argument`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Argument` instance manually + and attaching it to the :attr:`Command.params` list. + + For the default argument class, refer to :class:`Argument` and + :class:`Parameter` for descriptions of parameters. + + :param cls: the argument class to instantiate. This defaults to + :class:`Argument`. + :param param_decls: Passed as positional arguments to the constructor of + ``cls``. + :param attrs: Passed as keyword arguments to the constructor of ``cls``. + """ + if cls is None: + cls = Argument + + def decorator(f: FC) -> FC: + _param_memo(f, cls(param_decls, **attrs)) + return f + + return decorator + + +def option( + *param_decls: str, cls: type[Option] | None = None, **attrs: t.Any +) -> t.Callable[[FC], FC]: + """Attaches an option to the command. All positional arguments are + passed as parameter declarations to :class:`Option`; all keyword + arguments are forwarded unchanged (except ``cls``). + This is equivalent to creating an :class:`Option` instance manually + and attaching it to the :attr:`Command.params` list. + + For the default option class, refer to :class:`Option` and + :class:`Parameter` for descriptions of parameters. + + :param cls: the option class to instantiate. This defaults to + :class:`Option`. + :param param_decls: Passed as positional arguments to the constructor of + ``cls``. + :param attrs: Passed as keyword arguments to the constructor of ``cls``. + """ + if cls is None: + cls = Option + + def decorator(f: FC) -> FC: + _param_memo(f, cls(param_decls, **attrs)) + return f + + return decorator + + +def confirmation_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--yes`` option which shows a prompt before continuing if + not passed. If the prompt is declined, the program will exit. + + :param param_decls: One or more option names. Defaults to the single + value ``"--yes"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value: + ctx.abort() + + if not param_decls: + param_decls = ("--yes",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("callback", callback) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("prompt", "Do you want to continue?") + kwargs.setdefault("help", "Confirm the action without prompting.") + return option(*param_decls, **kwargs) + + +def password_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Add a ``--password`` option which prompts for a password, hiding + input and asking to enter the value again for confirmation. + + :param param_decls: One or more option names. Defaults to the single + value ``"--password"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + if not param_decls: + param_decls = ("--password",) + + kwargs.setdefault("prompt", True) + kwargs.setdefault("confirmation_prompt", True) + kwargs.setdefault("hide_input", True) + return option(*param_decls, **kwargs) + + +def version_option( + version: str | None = None, + *param_decls: str, + package_name: str | None = None, + prog_name: str | None = None, + message: str | None = None, + **kwargs: t.Any, +) -> t.Callable[[FC], FC]: + """Add a ``--version`` option which immediately prints the version + number and exits the program. + + If ``version`` is not provided, Click will try to detect it using + :func:`importlib.metadata.version` to get the version for the + ``package_name``. + + If ``package_name`` is not provided, Click will try to detect it by + inspecting the stack frames. This will be used to detect the + version, so it must match the name of the installed package. + + :param version: The version number to show. If not provided, Click + will try to detect it. + :param param_decls: One or more option names. Defaults to the single + value ``"--version"``. + :param package_name: The package name to detect the version from. If + not provided, Click will try to detect it. + :param prog_name: The name of the CLI to show in the message. If not + provided, it will be detected from the command. + :param message: The message to show. The values ``%(prog)s``, + ``%(package)s``, and ``%(version)s`` are available. Defaults to + ``"%(prog)s, version %(version)s"``. + :param kwargs: Extra arguments are passed to :func:`option`. + :raise RuntimeError: ``version`` could not be detected. + + .. versionchanged:: 8.0 + Add the ``package_name`` parameter, and the ``%(package)s`` + value for messages. + + .. versionchanged:: 8.0 + Use :mod:`importlib.metadata` instead of ``pkg_resources``. The + version is detected based on the package name, not the entry + point name. The Python package name must match the installed + package name, or be passed with ``package_name=``. + """ + if message is None: + message = _("%(prog)s, version %(version)s") + + if version is None and package_name is None: + frame = inspect.currentframe() + f_back = frame.f_back if frame is not None else None + f_globals = f_back.f_globals if f_back is not None else None + # break reference cycle + # https://docs.python.org/3/library/inspect.html#the-interpreter-stack + del frame + + if f_globals is not None: + package_name = f_globals.get("__name__") + + if package_name == "__main__": + package_name = f_globals.get("__package__") + + if package_name: + package_name = package_name.partition(".")[0] + + def callback(ctx: Context, param: Parameter, value: bool) -> None: + if not value or ctx.resilient_parsing: + return + + nonlocal prog_name + nonlocal version + + if prog_name is None: + prog_name = ctx.find_root().info_name + + if version is None and package_name is not None: + import importlib.metadata + + try: + version = importlib.metadata.version(package_name) + except importlib.metadata.PackageNotFoundError: + raise RuntimeError( + f"{package_name!r} is not installed. Try passing" + " 'package_name' instead." + ) from None + + if version is None: + raise RuntimeError( + f"Could not determine the version for {package_name!r} automatically." + ) + + echo( + message % {"prog": prog_name, "package": package_name, "version": version}, + color=ctx.color, + ) + ctx.exit() + + if not param_decls: + param_decls = ("--version",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show the version and exit.")) + kwargs["callback"] = callback + return option(*param_decls, **kwargs) + + +def help_option(*param_decls: str, **kwargs: t.Any) -> t.Callable[[FC], FC]: + """Pre-configured ``--help`` option which immediately prints the help page + and exits the program. + + :param param_decls: One or more option names. Defaults to the single + value ``"--help"``. + :param kwargs: Extra arguments are passed to :func:`option`. + """ + + def show_help(ctx: Context, param: Parameter, value: bool) -> None: + """Callback that print the help page on ```` and exits.""" + if value and not ctx.resilient_parsing: + echo(ctx.get_help(), color=ctx.color) + ctx.exit() + + if not param_decls: + param_decls = ("--help",) + + kwargs.setdefault("is_flag", True) + kwargs.setdefault("expose_value", False) + kwargs.setdefault("is_eager", True) + kwargs.setdefault("help", _("Show this message and exit.")) + kwargs.setdefault("callback", show_help) + + return option(*param_decls, **kwargs) diff --git a/netdeploy/lib/python3.11/site-packages/click/exceptions.py b/netdeploy/lib/python3.11/site-packages/click/exceptions.py new file mode 100644 index 0000000..4d782ee --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/exceptions.py @@ -0,0 +1,308 @@ +from __future__ import annotations + +import collections.abc as cabc +import typing as t +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import get_text_stderr +from .globals import resolve_color_default +from .utils import echo +from .utils import format_filename + +if t.TYPE_CHECKING: + from .core import Command + from .core import Context + from .core import Parameter + + +def _join_param_hints(param_hint: cabc.Sequence[str] | str | None) -> str | None: + if param_hint is not None and not isinstance(param_hint, str): + return " / ".join(repr(x) for x in param_hint) + + return param_hint + + +class ClickException(Exception): + """An exception that Click can handle and show to the user.""" + + #: The exit code for this exception. + exit_code = 1 + + def __init__(self, message: str) -> None: + super().__init__(message) + # The context will be removed by the time we print the message, so cache + # the color settings here to be used later on (in `show`) + self.show_color: bool | None = resolve_color_default() + self.message = message + + def format_message(self) -> str: + return self.message + + def __str__(self) -> str: + return self.message + + def show(self, file: t.IO[t.Any] | None = None) -> None: + if file is None: + file = get_text_stderr() + + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=self.show_color, + ) + + +class UsageError(ClickException): + """An internal exception that signals a usage error. This typically + aborts any further handling. + + :param message: the error message to display. + :param ctx: optionally the context that caused this error. Click will + fill in the context automatically in some situations. + """ + + exit_code = 2 + + def __init__(self, message: str, ctx: Context | None = None) -> None: + super().__init__(message) + self.ctx = ctx + self.cmd: Command | None = self.ctx.command if self.ctx else None + + def show(self, file: t.IO[t.Any] | None = None) -> None: + if file is None: + file = get_text_stderr() + color = None + hint = "" + if ( + self.ctx is not None + and self.ctx.command.get_help_option(self.ctx) is not None + ): + hint = _("Try '{command} {option}' for help.").format( + command=self.ctx.command_path, option=self.ctx.help_option_names[0] + ) + hint = f"{hint}\n" + if self.ctx is not None: + color = self.ctx.color + echo(f"{self.ctx.get_usage()}\n{hint}", file=file, color=color) + echo( + _("Error: {message}").format(message=self.format_message()), + file=file, + color=color, + ) + + +class BadParameter(UsageError): + """An exception that formats out a standardized error message for a + bad parameter. This is useful when thrown from a callback or type as + Click will attach contextual information to it (for instance, which + parameter it is). + + .. versionadded:: 2.0 + + :param param: the parameter object that caused this error. This can + be left out, and Click will attach this info itself + if possible. + :param param_hint: a string that shows up as parameter name. This + can be used as alternative to `param` in cases + where custom validation should happen. If it is + a string it's used as such, if it's a list then + each item is quoted and separated. + """ + + def __init__( + self, + message: str, + ctx: Context | None = None, + param: Parameter | None = None, + param_hint: cabc.Sequence[str] | str | None = None, + ) -> None: + super().__init__(message, ctx) + self.param = param + self.param_hint = param_hint + + def format_message(self) -> str: + if self.param_hint is not None: + param_hint = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) # type: ignore + else: + return _("Invalid value: {message}").format(message=self.message) + + return _("Invalid value for {param_hint}: {message}").format( + param_hint=_join_param_hints(param_hint), message=self.message + ) + + +class MissingParameter(BadParameter): + """Raised if click required an option or argument but it was not + provided when invoking the script. + + .. versionadded:: 4.0 + + :param param_type: a string that indicates the type of the parameter. + The default is to inherit the parameter type from + the given `param`. Valid values are ``'parameter'``, + ``'option'`` or ``'argument'``. + """ + + def __init__( + self, + message: str | None = None, + ctx: Context | None = None, + param: Parameter | None = None, + param_hint: cabc.Sequence[str] | str | None = None, + param_type: str | None = None, + ) -> None: + super().__init__(message or "", ctx, param, param_hint) + self.param_type = param_type + + def format_message(self) -> str: + if self.param_hint is not None: + param_hint: cabc.Sequence[str] | str | None = self.param_hint + elif self.param is not None: + param_hint = self.param.get_error_hint(self.ctx) # type: ignore + else: + param_hint = None + + param_hint = _join_param_hints(param_hint) + param_hint = f" {param_hint}" if param_hint else "" + + param_type = self.param_type + if param_type is None and self.param is not None: + param_type = self.param.param_type_name + + msg = self.message + if self.param is not None: + msg_extra = self.param.type.get_missing_message( + param=self.param, ctx=self.ctx + ) + if msg_extra: + if msg: + msg += f". {msg_extra}" + else: + msg = msg_extra + + msg = f" {msg}" if msg else "" + + # Translate param_type for known types. + if param_type == "argument": + missing = _("Missing argument") + elif param_type == "option": + missing = _("Missing option") + elif param_type == "parameter": + missing = _("Missing parameter") + else: + missing = _("Missing {param_type}").format(param_type=param_type) + + return f"{missing}{param_hint}.{msg}" + + def __str__(self) -> str: + if not self.message: + param_name = self.param.name if self.param else None + return _("Missing parameter: {param_name}").format(param_name=param_name) + else: + return self.message + + +class NoSuchOption(UsageError): + """Raised if click attempted to handle an option that does not + exist. + + .. versionadded:: 4.0 + """ + + def __init__( + self, + option_name: str, + message: str | None = None, + possibilities: cabc.Sequence[str] | None = None, + ctx: Context | None = None, + ) -> None: + if message is None: + message = _("No such option: {name}").format(name=option_name) + + super().__init__(message, ctx) + self.option_name = option_name + self.possibilities = possibilities + + def format_message(self) -> str: + if not self.possibilities: + return self.message + + possibility_str = ", ".join(sorted(self.possibilities)) + suggest = ngettext( + "Did you mean {possibility}?", + "(Possible options: {possibilities})", + len(self.possibilities), + ).format(possibility=possibility_str, possibilities=possibility_str) + return f"{self.message} {suggest}" + + +class BadOptionUsage(UsageError): + """Raised if an option is generally supplied but the use of the option + was incorrect. This is for instance raised if the number of arguments + for an option is not correct. + + .. versionadded:: 4.0 + + :param option_name: the name of the option being used incorrectly. + """ + + def __init__( + self, option_name: str, message: str, ctx: Context | None = None + ) -> None: + super().__init__(message, ctx) + self.option_name = option_name + + +class BadArgumentUsage(UsageError): + """Raised if an argument is generally supplied but the use of the argument + was incorrect. This is for instance raised if the number of values + for an argument is not correct. + + .. versionadded:: 6.0 + """ + + +class NoArgsIsHelpError(UsageError): + def __init__(self, ctx: Context) -> None: + self.ctx: Context + super().__init__(ctx.get_help(), ctx=ctx) + + def show(self, file: t.IO[t.Any] | None = None) -> None: + echo(self.format_message(), file=file, err=True, color=self.ctx.color) + + +class FileError(ClickException): + """Raised if a file cannot be opened.""" + + def __init__(self, filename: str, hint: str | None = None) -> None: + if hint is None: + hint = _("unknown error") + + super().__init__(hint) + self.ui_filename: str = format_filename(filename) + self.filename = filename + + def format_message(self) -> str: + return _("Could not open file {filename!r}: {message}").format( + filename=self.ui_filename, message=self.message + ) + + +class Abort(RuntimeError): + """An internal signalling exception that signals Click to abort.""" + + +class Exit(RuntimeError): + """An exception that indicates that the application should exit with some + status code. + + :param code: the status code to exit with. + """ + + __slots__ = ("exit_code",) + + def __init__(self, code: int = 0) -> None: + self.exit_code: int = code diff --git a/netdeploy/lib/python3.11/site-packages/click/formatting.py b/netdeploy/lib/python3.11/site-packages/click/formatting.py new file mode 100644 index 0000000..0b64f83 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/formatting.py @@ -0,0 +1,301 @@ +from __future__ import annotations + +import collections.abc as cabc +from contextlib import contextmanager +from gettext import gettext as _ + +from ._compat import term_len +from .parser import _split_opt + +# Can force a width. This is used by the test system +FORCED_WIDTH: int | None = None + + +def measure_table(rows: cabc.Iterable[tuple[str, str]]) -> tuple[int, ...]: + widths: dict[int, int] = {} + + for row in rows: + for idx, col in enumerate(row): + widths[idx] = max(widths.get(idx, 0), term_len(col)) + + return tuple(y for x, y in sorted(widths.items())) + + +def iter_rows( + rows: cabc.Iterable[tuple[str, str]], col_count: int +) -> cabc.Iterator[tuple[str, ...]]: + for row in rows: + yield row + ("",) * (col_count - len(row)) + + +def wrap_text( + text: str, + width: int = 78, + initial_indent: str = "", + subsequent_indent: str = "", + preserve_paragraphs: bool = False, +) -> str: + """A helper function that intelligently wraps text. By default, it + assumes that it operates on a single paragraph of text but if the + `preserve_paragraphs` parameter is provided it will intelligently + handle paragraphs (defined by two empty lines). + + If paragraphs are handled, a paragraph can be prefixed with an empty + line containing the ``\\b`` character (``\\x08``) to indicate that + no rewrapping should happen in that block. + + :param text: the text that should be rewrapped. + :param width: the maximum width for the text. + :param initial_indent: the initial indent that should be placed on the + first line as a string. + :param subsequent_indent: the indent string that should be placed on + each consecutive line. + :param preserve_paragraphs: if this flag is set then the wrapping will + intelligently handle paragraphs. + """ + from ._textwrap import TextWrapper + + text = text.expandtabs() + wrapper = TextWrapper( + width, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + replace_whitespace=False, + ) + if not preserve_paragraphs: + return wrapper.fill(text) + + p: list[tuple[int, bool, str]] = [] + buf: list[str] = [] + indent = None + + def _flush_par() -> None: + if not buf: + return + if buf[0].strip() == "\b": + p.append((indent or 0, True, "\n".join(buf[1:]))) + else: + p.append((indent or 0, False, " ".join(buf))) + del buf[:] + + for line in text.splitlines(): + if not line: + _flush_par() + indent = None + else: + if indent is None: + orig_len = term_len(line) + line = line.lstrip() + indent = orig_len - term_len(line) + buf.append(line) + _flush_par() + + rv = [] + for indent, raw, text in p: + with wrapper.extra_indent(" " * indent): + if raw: + rv.append(wrapper.indent_only(text)) + else: + rv.append(wrapper.fill(text)) + + return "\n\n".join(rv) + + +class HelpFormatter: + """This class helps with formatting text-based help pages. It's + usually just needed for very special internal cases, but it's also + exposed so that developers can write their own fancy outputs. + + At present, it always writes into memory. + + :param indent_increment: the additional increment for each level. + :param width: the width for the text. This defaults to the terminal + width clamped to a maximum of 78. + """ + + def __init__( + self, + indent_increment: int = 2, + width: int | None = None, + max_width: int | None = None, + ) -> None: + self.indent_increment = indent_increment + if max_width is None: + max_width = 80 + if width is None: + import shutil + + width = FORCED_WIDTH + if width is None: + width = max(min(shutil.get_terminal_size().columns, max_width) - 2, 50) + self.width = width + self.current_indent: int = 0 + self.buffer: list[str] = [] + + def write(self, string: str) -> None: + """Writes a unicode string into the internal buffer.""" + self.buffer.append(string) + + def indent(self) -> None: + """Increases the indentation.""" + self.current_indent += self.indent_increment + + def dedent(self) -> None: + """Decreases the indentation.""" + self.current_indent -= self.indent_increment + + def write_usage(self, prog: str, args: str = "", prefix: str | None = None) -> None: + """Writes a usage line into the buffer. + + :param prog: the program name. + :param args: whitespace separated list of arguments. + :param prefix: The prefix for the first line. Defaults to + ``"Usage: "``. + """ + if prefix is None: + prefix = f"{_('Usage:')} " + + usage_prefix = f"{prefix:>{self.current_indent}}{prog} " + text_width = self.width - self.current_indent + + if text_width >= (term_len(usage_prefix) + 20): + # The arguments will fit to the right of the prefix. + indent = " " * term_len(usage_prefix) + self.write( + wrap_text( + args, + text_width, + initial_indent=usage_prefix, + subsequent_indent=indent, + ) + ) + else: + # The prefix is too long, put the arguments on the next line. + self.write(usage_prefix) + self.write("\n") + indent = " " * (max(self.current_indent, term_len(prefix)) + 4) + self.write( + wrap_text( + args, text_width, initial_indent=indent, subsequent_indent=indent + ) + ) + + self.write("\n") + + def write_heading(self, heading: str) -> None: + """Writes a heading into the buffer.""" + self.write(f"{'':>{self.current_indent}}{heading}:\n") + + def write_paragraph(self) -> None: + """Writes a paragraph into the buffer.""" + if self.buffer: + self.write("\n") + + def write_text(self, text: str) -> None: + """Writes re-indented text into the buffer. This rewraps and + preserves paragraphs. + """ + indent = " " * self.current_indent + self.write( + wrap_text( + text, + self.width, + initial_indent=indent, + subsequent_indent=indent, + preserve_paragraphs=True, + ) + ) + self.write("\n") + + def write_dl( + self, + rows: cabc.Sequence[tuple[str, str]], + col_max: int = 30, + col_spacing: int = 2, + ) -> None: + """Writes a definition list into the buffer. This is how options + and commands are usually formatted. + + :param rows: a list of two item tuples for the terms and values. + :param col_max: the maximum width of the first column. + :param col_spacing: the number of spaces between the first and + second column. + """ + rows = list(rows) + widths = measure_table(rows) + if len(widths) != 2: + raise TypeError("Expected two columns for definition list") + + first_col = min(widths[0], col_max) + col_spacing + + for first, second in iter_rows(rows, len(widths)): + self.write(f"{'':>{self.current_indent}}{first}") + if not second: + self.write("\n") + continue + if term_len(first) <= first_col - col_spacing: + self.write(" " * (first_col - term_len(first))) + else: + self.write("\n") + self.write(" " * (first_col + self.current_indent)) + + text_width = max(self.width - first_col - 2, 10) + wrapped_text = wrap_text(second, text_width, preserve_paragraphs=True) + lines = wrapped_text.splitlines() + + if lines: + self.write(f"{lines[0]}\n") + + for line in lines[1:]: + self.write(f"{'':>{first_col + self.current_indent}}{line}\n") + else: + self.write("\n") + + @contextmanager + def section(self, name: str) -> cabc.Iterator[None]: + """Helpful context manager that writes a paragraph, a heading, + and the indents. + + :param name: the section name that is written as heading. + """ + self.write_paragraph() + self.write_heading(name) + self.indent() + try: + yield + finally: + self.dedent() + + @contextmanager + def indentation(self) -> cabc.Iterator[None]: + """A context manager that increases the indentation.""" + self.indent() + try: + yield + finally: + self.dedent() + + def getvalue(self) -> str: + """Returns the buffer contents.""" + return "".join(self.buffer) + + +def join_options(options: cabc.Sequence[str]) -> tuple[str, bool]: + """Given a list of option strings this joins them in the most appropriate + way and returns them in the form ``(formatted_string, + any_prefix_is_slash)`` where the second item in the tuple is a flag that + indicates if any of the option prefixes was a slash. + """ + rv = [] + any_prefix_is_slash = False + + for opt in options: + prefix = _split_opt(opt)[0] + + if prefix == "/": + any_prefix_is_slash = True + + rv.append((len(prefix), opt)) + + rv.sort(key=lambda x: x[0]) + return ", ".join(x[1] for x in rv), any_prefix_is_slash diff --git a/netdeploy/lib/python3.11/site-packages/click/globals.py b/netdeploy/lib/python3.11/site-packages/click/globals.py new file mode 100644 index 0000000..a2f9172 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/globals.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +import typing as t +from threading import local + +if t.TYPE_CHECKING: + from .core import Context + +_local = local() + + +@t.overload +def get_current_context(silent: t.Literal[False] = False) -> Context: ... + + +@t.overload +def get_current_context(silent: bool = ...) -> Context | None: ... + + +def get_current_context(silent: bool = False) -> Context | None: + """Returns the current click context. This can be used as a way to + access the current context object from anywhere. This is a more implicit + alternative to the :func:`pass_context` decorator. This function is + primarily useful for helpers such as :func:`echo` which might be + interested in changing its behavior based on the current context. + + To push the current context, :meth:`Context.scope` can be used. + + .. versionadded:: 5.0 + + :param silent: if set to `True` the return value is `None` if no context + is available. The default behavior is to raise a + :exc:`RuntimeError`. + """ + try: + return t.cast("Context", _local.stack[-1]) + except (AttributeError, IndexError) as e: + if not silent: + raise RuntimeError("There is no active click context.") from e + + return None + + +def push_context(ctx: Context) -> None: + """Pushes a new context to the current stack.""" + _local.__dict__.setdefault("stack", []).append(ctx) + + +def pop_context() -> None: + """Removes the top level from the stack.""" + _local.stack.pop() + + +def resolve_color_default(color: bool | None = None) -> bool | None: + """Internal helper to get the default value of the color flag. If a + value is passed it's returned unchanged, otherwise it's looked up from + the current context. + """ + if color is not None: + return color + + ctx = get_current_context(silent=True) + + if ctx is not None: + return ctx.color + + return None diff --git a/netdeploy/lib/python3.11/site-packages/click/parser.py b/netdeploy/lib/python3.11/site-packages/click/parser.py new file mode 100644 index 0000000..1ea1f71 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/parser.py @@ -0,0 +1,532 @@ +""" +This module started out as largely a copy paste from the stdlib's +optparse module with the features removed that we do not need from +optparse because we implement them in Click on a higher level (for +instance type handling, help formatting and a lot more). + +The plan is to remove more and more from here over time. + +The reason this is a different module and not optparse from the stdlib +is that there are differences in 2.x and 3.x about the error messages +generated and optparse in the stdlib uses gettext for no good reason +and might cause us issues. + +Click uses parts of optparse written by Gregory P. Ward and maintained +by the Python Software Foundation. This is limited to code in parser.py. + +Copyright 2001-2006 Gregory P. Ward. All rights reserved. +Copyright 2002-2006 Python Software Foundation. All rights reserved. +""" + +# This code uses parts of optparse written by Gregory P. Ward and +# maintained by the Python Software Foundation. +# Copyright 2001-2006 Gregory P. Ward +# Copyright 2002-2006 Python Software Foundation +from __future__ import annotations + +import collections.abc as cabc +import typing as t +from collections import deque +from gettext import gettext as _ +from gettext import ngettext + +from ._utils import FLAG_NEEDS_VALUE +from ._utils import UNSET +from .exceptions import BadArgumentUsage +from .exceptions import BadOptionUsage +from .exceptions import NoSuchOption +from .exceptions import UsageError + +if t.TYPE_CHECKING: + from ._utils import T_FLAG_NEEDS_VALUE + from ._utils import T_UNSET + from .core import Argument as CoreArgument + from .core import Context + from .core import Option as CoreOption + from .core import Parameter as CoreParameter + +V = t.TypeVar("V") + + +def _unpack_args( + args: cabc.Sequence[str], nargs_spec: cabc.Sequence[int] +) -> tuple[cabc.Sequence[str | cabc.Sequence[str | None] | None], list[str]]: + """Given an iterable of arguments and an iterable of nargs specifications, + it returns a tuple with all the unpacked arguments at the first index + and all remaining arguments as the second. + + The nargs specification is the number of arguments that should be consumed + or `-1` to indicate that this position should eat up all the remainders. + + Missing items are filled with ``UNSET``. + """ + args = deque(args) + nargs_spec = deque(nargs_spec) + rv: list[str | tuple[str | T_UNSET, ...] | T_UNSET] = [] + spos: int | None = None + + def _fetch(c: deque[V]) -> V | T_UNSET: + try: + if spos is None: + return c.popleft() + else: + return c.pop() + except IndexError: + return UNSET + + while nargs_spec: + nargs = _fetch(nargs_spec) + + if nargs is None: + continue + + if nargs == 1: + rv.append(_fetch(args)) # type: ignore[arg-type] + elif nargs > 1: + x = [_fetch(args) for _ in range(nargs)] + + # If we're reversed, we're pulling in the arguments in reverse, + # so we need to turn them around. + if spos is not None: + x.reverse() + + rv.append(tuple(x)) + elif nargs < 0: + if spos is not None: + raise TypeError("Cannot have two nargs < 0") + + spos = len(rv) + rv.append(UNSET) + + # spos is the position of the wildcard (star). If it's not `None`, + # we fill it with the remainder. + if spos is not None: + rv[spos] = tuple(args) + args = [] + rv[spos + 1 :] = reversed(rv[spos + 1 :]) + + return tuple(rv), list(args) + + +def _split_opt(opt: str) -> tuple[str, str]: + first = opt[:1] + if first.isalnum(): + return "", opt + if opt[1:2] == first: + return opt[:2], opt[2:] + return first, opt[1:] + + +def _normalize_opt(opt: str, ctx: Context | None) -> str: + if ctx is None or ctx.token_normalize_func is None: + return opt + prefix, opt = _split_opt(opt) + return f"{prefix}{ctx.token_normalize_func(opt)}" + + +class _Option: + def __init__( + self, + obj: CoreOption, + opts: cabc.Sequence[str], + dest: str | None, + action: str | None = None, + nargs: int = 1, + const: t.Any | None = None, + ): + self._short_opts = [] + self._long_opts = [] + self.prefixes: set[str] = set() + + for opt in opts: + prefix, value = _split_opt(opt) + if not prefix: + raise ValueError(f"Invalid start character for option ({opt})") + self.prefixes.add(prefix[0]) + if len(prefix) == 1 and len(value) == 1: + self._short_opts.append(opt) + else: + self._long_opts.append(opt) + self.prefixes.add(prefix) + + if action is None: + action = "store" + + self.dest = dest + self.action = action + self.nargs = nargs + self.const = const + self.obj = obj + + @property + def takes_value(self) -> bool: + return self.action in ("store", "append") + + def process(self, value: t.Any, state: _ParsingState) -> None: + if self.action == "store": + state.opts[self.dest] = value # type: ignore + elif self.action == "store_const": + state.opts[self.dest] = self.const # type: ignore + elif self.action == "append": + state.opts.setdefault(self.dest, []).append(value) # type: ignore + elif self.action == "append_const": + state.opts.setdefault(self.dest, []).append(self.const) # type: ignore + elif self.action == "count": + state.opts[self.dest] = state.opts.get(self.dest, 0) + 1 # type: ignore + else: + raise ValueError(f"unknown action '{self.action}'") + state.order.append(self.obj) + + +class _Argument: + def __init__(self, obj: CoreArgument, dest: str | None, nargs: int = 1): + self.dest = dest + self.nargs = nargs + self.obj = obj + + def process( + self, + value: str | cabc.Sequence[str | None] | None | T_UNSET, + state: _ParsingState, + ) -> None: + if self.nargs > 1: + assert isinstance(value, cabc.Sequence) + holes = sum(1 for x in value if x is UNSET) + if holes == len(value): + value = UNSET + elif holes != 0: + raise BadArgumentUsage( + _("Argument {name!r} takes {nargs} values.").format( + name=self.dest, nargs=self.nargs + ) + ) + + # We failed to collect any argument value so we consider the argument as unset. + if value == (): + value = UNSET + + state.opts[self.dest] = value # type: ignore + state.order.append(self.obj) + + +class _ParsingState: + def __init__(self, rargs: list[str]) -> None: + self.opts: dict[str, t.Any] = {} + self.largs: list[str] = [] + self.rargs = rargs + self.order: list[CoreParameter] = [] + + +class _OptionParser: + """The option parser is an internal class that is ultimately used to + parse options and arguments. It's modelled after optparse and brings + a similar but vastly simplified API. It should generally not be used + directly as the high level Click classes wrap it for you. + + It's not nearly as extensible as optparse or argparse as it does not + implement features that are implemented on a higher level (such as + types or defaults). + + :param ctx: optionally the :class:`~click.Context` where this parser + should go with. + + .. deprecated:: 8.2 + Will be removed in Click 9.0. + """ + + def __init__(self, ctx: Context | None = None) -> None: + #: The :class:`~click.Context` for this parser. This might be + #: `None` for some advanced use cases. + self.ctx = ctx + #: This controls how the parser deals with interspersed arguments. + #: If this is set to `False`, the parser will stop on the first + #: non-option. Click uses this to implement nested subcommands + #: safely. + self.allow_interspersed_args: bool = True + #: This tells the parser how to deal with unknown options. By + #: default it will error out (which is sensible), but there is a + #: second mode where it will ignore it and continue processing + #: after shifting all the unknown options into the resulting args. + self.ignore_unknown_options: bool = False + + if ctx is not None: + self.allow_interspersed_args = ctx.allow_interspersed_args + self.ignore_unknown_options = ctx.ignore_unknown_options + + self._short_opt: dict[str, _Option] = {} + self._long_opt: dict[str, _Option] = {} + self._opt_prefixes = {"-", "--"} + self._args: list[_Argument] = [] + + def add_option( + self, + obj: CoreOption, + opts: cabc.Sequence[str], + dest: str | None, + action: str | None = None, + nargs: int = 1, + const: t.Any | None = None, + ) -> None: + """Adds a new option named `dest` to the parser. The destination + is not inferred (unlike with optparse) and needs to be explicitly + provided. Action can be any of ``store``, ``store_const``, + ``append``, ``append_const`` or ``count``. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + opts = [_normalize_opt(opt, self.ctx) for opt in opts] + option = _Option(obj, opts, dest, action=action, nargs=nargs, const=const) + self._opt_prefixes.update(option.prefixes) + for opt in option._short_opts: + self._short_opt[opt] = option + for opt in option._long_opts: + self._long_opt[opt] = option + + def add_argument(self, obj: CoreArgument, dest: str | None, nargs: int = 1) -> None: + """Adds a positional argument named `dest` to the parser. + + The `obj` can be used to identify the option in the order list + that is returned from the parser. + """ + self._args.append(_Argument(obj, dest=dest, nargs=nargs)) + + def parse_args( + self, args: list[str] + ) -> tuple[dict[str, t.Any], list[str], list[CoreParameter]]: + """Parses positional arguments and returns ``(values, args, order)`` + for the parsed options and arguments as well as the leftover + arguments if there are any. The order is a list of objects as they + appear on the command line. If arguments appear multiple times they + will be memorized multiple times as well. + """ + state = _ParsingState(args) + try: + self._process_args_for_options(state) + self._process_args_for_args(state) + except UsageError: + if self.ctx is None or not self.ctx.resilient_parsing: + raise + return state.opts, state.largs, state.order + + def _process_args_for_args(self, state: _ParsingState) -> None: + pargs, args = _unpack_args( + state.largs + state.rargs, [x.nargs for x in self._args] + ) + + for idx, arg in enumerate(self._args): + arg.process(pargs[idx], state) + + state.largs = args + state.rargs = [] + + def _process_args_for_options(self, state: _ParsingState) -> None: + while state.rargs: + arg = state.rargs.pop(0) + arglen = len(arg) + # Double dashes always handled explicitly regardless of what + # prefixes are valid. + if arg == "--": + return + elif arg[:1] in self._opt_prefixes and arglen > 1: + self._process_opts(arg, state) + elif self.allow_interspersed_args: + state.largs.append(arg) + else: + state.rargs.insert(0, arg) + return + + # Say this is the original argument list: + # [arg0, arg1, ..., arg(i-1), arg(i), arg(i+1), ..., arg(N-1)] + # ^ + # (we are about to process arg(i)). + # + # Then rargs is [arg(i), ..., arg(N-1)] and largs is a *subset* of + # [arg0, ..., arg(i-1)] (any options and their arguments will have + # been removed from largs). + # + # The while loop will usually consume 1 or more arguments per pass. + # If it consumes 1 (eg. arg is an option that takes no arguments), + # then after _process_arg() is done the situation is: + # + # largs = subset of [arg0, ..., arg(i)] + # rargs = [arg(i+1), ..., arg(N-1)] + # + # If allow_interspersed_args is false, largs will always be + # *empty* -- still a subset of [arg0, ..., arg(i-1)], but + # not a very interesting subset! + + def _match_long_opt( + self, opt: str, explicit_value: str | None, state: _ParsingState + ) -> None: + if opt not in self._long_opt: + from difflib import get_close_matches + + possibilities = get_close_matches(opt, self._long_opt) + raise NoSuchOption(opt, possibilities=possibilities, ctx=self.ctx) + + option = self._long_opt[opt] + if option.takes_value: + # At this point it's safe to modify rargs by injecting the + # explicit value, because no exception is raised in this + # branch. This means that the inserted value will be fully + # consumed. + if explicit_value is not None: + state.rargs.insert(0, explicit_value) + + value = self._get_value_from_state(opt, option, state) + + elif explicit_value is not None: + raise BadOptionUsage( + opt, _("Option {name!r} does not take a value.").format(name=opt) + ) + + else: + value = UNSET + + option.process(value, state) + + def _match_short_opt(self, arg: str, state: _ParsingState) -> None: + stop = False + i = 1 + prefix = arg[0] + unknown_options = [] + + for ch in arg[1:]: + opt = _normalize_opt(f"{prefix}{ch}", self.ctx) + option = self._short_opt.get(opt) + i += 1 + + if not option: + if self.ignore_unknown_options: + unknown_options.append(ch) + continue + raise NoSuchOption(opt, ctx=self.ctx) + if option.takes_value: + # Any characters left in arg? Pretend they're the + # next arg, and stop consuming characters of arg. + if i < len(arg): + state.rargs.insert(0, arg[i:]) + stop = True + + value = self._get_value_from_state(opt, option, state) + + else: + value = UNSET + + option.process(value, state) + + if stop: + break + + # If we got any unknown options we recombine the string of the + # remaining options and re-attach the prefix, then report that + # to the state as new larg. This way there is basic combinatorics + # that can be achieved while still ignoring unknown arguments. + if self.ignore_unknown_options and unknown_options: + state.largs.append(f"{prefix}{''.join(unknown_options)}") + + def _get_value_from_state( + self, option_name: str, option: _Option, state: _ParsingState + ) -> str | cabc.Sequence[str] | T_FLAG_NEEDS_VALUE: + nargs = option.nargs + + value: str | cabc.Sequence[str] | T_FLAG_NEEDS_VALUE + + if len(state.rargs) < nargs: + if option.obj._flag_needs_value: + # Option allows omitting the value. + value = FLAG_NEEDS_VALUE + else: + raise BadOptionUsage( + option_name, + ngettext( + "Option {name!r} requires an argument.", + "Option {name!r} requires {nargs} arguments.", + nargs, + ).format(name=option_name, nargs=nargs), + ) + elif nargs == 1: + next_rarg = state.rargs[0] + + if ( + option.obj._flag_needs_value + and isinstance(next_rarg, str) + and next_rarg[:1] in self._opt_prefixes + and len(next_rarg) > 1 + ): + # The next arg looks like the start of an option, don't + # use it as the value if omitting the value is allowed. + value = FLAG_NEEDS_VALUE + else: + value = state.rargs.pop(0) + else: + value = tuple(state.rargs[:nargs]) + del state.rargs[:nargs] + + return value + + def _process_opts(self, arg: str, state: _ParsingState) -> None: + explicit_value = None + # Long option handling happens in two parts. The first part is + # supporting explicitly attached values. In any case, we will try + # to long match the option first. + if "=" in arg: + long_opt, explicit_value = arg.split("=", 1) + else: + long_opt = arg + norm_long_opt = _normalize_opt(long_opt, self.ctx) + + # At this point we will match the (assumed) long option through + # the long option matching code. Note that this allows options + # like "-foo" to be matched as long options. + try: + self._match_long_opt(norm_long_opt, explicit_value, state) + except NoSuchOption: + # At this point the long option matching failed, and we need + # to try with short options. However there is a special rule + # which says, that if we have a two character options prefix + # (applies to "--foo" for instance), we do not dispatch to the + # short option code and will instead raise the no option + # error. + if arg[:2] not in self._opt_prefixes: + self._match_short_opt(arg, state) + return + + if not self.ignore_unknown_options: + raise + + state.largs.append(arg) + + +def __getattr__(name: str) -> object: + import warnings + + if name in { + "OptionParser", + "Argument", + "Option", + "split_opt", + "normalize_opt", + "ParsingState", + }: + warnings.warn( + f"'parser.{name}' is deprecated and will be removed in Click 9.0." + " The old parser is available in 'optparse'.", + DeprecationWarning, + stacklevel=2, + ) + return globals()[f"_{name}"] + + if name == "split_arg_string": + from .shell_completion import split_arg_string + + warnings.warn( + "Importing 'parser.split_arg_string' is deprecated, it will only be" + " available in 'shell_completion' in Click 9.0.", + DeprecationWarning, + stacklevel=2, + ) + return split_arg_string + + raise AttributeError(name) diff --git a/netdeploy/lib/python3.11/site-packages/click/py.typed b/netdeploy/lib/python3.11/site-packages/click/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/click/shell_completion.py b/netdeploy/lib/python3.11/site-packages/click/shell_completion.py new file mode 100644 index 0000000..8f1564c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/shell_completion.py @@ -0,0 +1,667 @@ +from __future__ import annotations + +import collections.abc as cabc +import os +import re +import typing as t +from gettext import gettext as _ + +from .core import Argument +from .core import Command +from .core import Context +from .core import Group +from .core import Option +from .core import Parameter +from .core import ParameterSource +from .utils import echo + + +def shell_complete( + cli: Command, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str, + instruction: str, +) -> int: + """Perform shell completion for the given CLI program. + + :param cli: Command being called. + :param ctx_args: Extra arguments to pass to + ``cli.make_context``. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + :param instruction: Value of ``complete_var`` with the completion + instruction and shell, in the form ``instruction_shell``. + :return: Status code to exit with. + """ + shell, _, instruction = instruction.partition("_") + comp_cls = get_completion_class(shell) + + if comp_cls is None: + return 1 + + comp = comp_cls(cli, ctx_args, prog_name, complete_var) + + if instruction == "source": + echo(comp.source()) + return 0 + + if instruction == "complete": + echo(comp.complete()) + return 0 + + return 1 + + +class CompletionItem: + """Represents a completion value and metadata about the value. The + default metadata is ``type`` to indicate special shell handling, + and ``help`` if a shell supports showing a help string next to the + value. + + Arbitrary parameters can be passed when creating the object, and + accessed using ``item.attr``. If an attribute wasn't passed, + accessing it returns ``None``. + + :param value: The completion suggestion. + :param type: Tells the shell script to provide special completion + support for the type. Click uses ``"dir"`` and ``"file"``. + :param help: String shown next to the value if supported. + :param kwargs: Arbitrary metadata. The built-in implementations + don't use this, but custom type completions paired with custom + shell support could use it. + """ + + __slots__ = ("value", "type", "help", "_info") + + def __init__( + self, + value: t.Any, + type: str = "plain", + help: str | None = None, + **kwargs: t.Any, + ) -> None: + self.value: t.Any = value + self.type: str = type + self.help: str | None = help + self._info = kwargs + + def __getattr__(self, name: str) -> t.Any: + return self._info.get(name) + + +# Only Bash >= 4.4 has the nosort option. +_SOURCE_BASH = """\ +%(complete_func)s() { + local IFS=$'\\n' + local response + + response=$(env COMP_WORDS="${COMP_WORDS[*]}" COMP_CWORD=$COMP_CWORD \ +%(complete_var)s=bash_complete $1) + + for completion in $response; do + IFS=',' read type value <<< "$completion" + + if [[ $type == 'dir' ]]; then + COMPREPLY=() + compopt -o dirnames + elif [[ $type == 'file' ]]; then + COMPREPLY=() + compopt -o default + elif [[ $type == 'plain' ]]; then + COMPREPLY+=($value) + fi + done + + return 0 +} + +%(complete_func)s_setup() { + complete -o nosort -F %(complete_func)s %(prog_name)s +} + +%(complete_func)s_setup; +""" + +# See ZshComplete.format_completion below, and issue #2703, before +# changing this script. +# +# (TL;DR: _describe is picky about the format, but this Zsh script snippet +# is already widely deployed. So freeze this script, and use clever-ish +# handling of colons in ZshComplet.format_completion.) +_SOURCE_ZSH = """\ +#compdef %(prog_name)s + +%(complete_func)s() { + local -a completions + local -a completions_with_descriptions + local -a response + (( ! $+commands[%(prog_name)s] )) && return 1 + + response=("${(@f)$(env COMP_WORDS="${words[*]}" COMP_CWORD=$((CURRENT-1)) \ +%(complete_var)s=zsh_complete %(prog_name)s)}") + + for type key descr in ${response}; do + if [[ "$type" == "plain" ]]; then + if [[ "$descr" == "_" ]]; then + completions+=("$key") + else + completions_with_descriptions+=("$key":"$descr") + fi + elif [[ "$type" == "dir" ]]; then + _path_files -/ + elif [[ "$type" == "file" ]]; then + _path_files -f + fi + done + + if [ -n "$completions_with_descriptions" ]; then + _describe -V unsorted completions_with_descriptions -U + fi + + if [ -n "$completions" ]; then + compadd -U -V unsorted -a completions + fi +} + +if [[ $zsh_eval_context[-1] == loadautofunc ]]; then + # autoload from fpath, call function directly + %(complete_func)s "$@" +else + # eval/source/. command, register function for later + compdef %(complete_func)s %(prog_name)s +fi +""" + +_SOURCE_FISH = """\ +function %(complete_func)s; + set -l response (env %(complete_var)s=fish_complete COMP_WORDS=(commandline -cp) \ +COMP_CWORD=(commandline -t) %(prog_name)s); + + for completion in $response; + set -l metadata (string split "," $completion); + + if test $metadata[1] = "dir"; + __fish_complete_directories $metadata[2]; + else if test $metadata[1] = "file"; + __fish_complete_path $metadata[2]; + else if test $metadata[1] = "plain"; + echo $metadata[2]; + end; + end; +end; + +complete --no-files --command %(prog_name)s --arguments \ +"(%(complete_func)s)"; +""" + + +class ShellComplete: + """Base class for providing shell completion support. A subclass for + a given shell will override attributes and methods to implement the + completion instructions (``source`` and ``complete``). + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param complete_var: Name of the environment variable that holds + the completion instruction. + + .. versionadded:: 8.0 + """ + + name: t.ClassVar[str] + """Name to register the shell as with :func:`add_completion_class`. + This is used in completion instructions (``{name}_source`` and + ``{name}_complete``). + """ + + source_template: t.ClassVar[str] + """Completion script template formatted by :meth:`source`. This must + be provided by subclasses. + """ + + def __init__( + self, + cli: Command, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + complete_var: str, + ) -> None: + self.cli = cli + self.ctx_args = ctx_args + self.prog_name = prog_name + self.complete_var = complete_var + + @property + def func_name(self) -> str: + """The name of the shell function defined by the completion + script. + """ + safe_name = re.sub(r"\W*", "", self.prog_name.replace("-", "_"), flags=re.ASCII) + return f"_{safe_name}_completion" + + def source_vars(self) -> dict[str, t.Any]: + """Vars for formatting :attr:`source_template`. + + By default this provides ``complete_func``, ``complete_var``, + and ``prog_name``. + """ + return { + "complete_func": self.func_name, + "complete_var": self.complete_var, + "prog_name": self.prog_name, + } + + def source(self) -> str: + """Produce the shell script that defines the completion + function. By default this ``%``-style formats + :attr:`source_template` with the dict returned by + :meth:`source_vars`. + """ + return self.source_template % self.source_vars() + + def get_completion_args(self) -> tuple[list[str], str]: + """Use the env vars defined by the shell script to return a + tuple of ``args, incomplete``. This must be implemented by + subclasses. + """ + raise NotImplementedError + + def get_completions(self, args: list[str], incomplete: str) -> list[CompletionItem]: + """Determine the context and last complete command or parameter + from the complete args. Call that object's ``shell_complete`` + method to get the completions for the incomplete value. + + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + ctx = _resolve_context(self.cli, self.ctx_args, self.prog_name, args) + obj, incomplete = _resolve_incomplete(ctx, args, incomplete) + return obj.shell_complete(ctx, incomplete) + + def format_completion(self, item: CompletionItem) -> str: + """Format a completion item into the form recognized by the + shell script. This must be implemented by subclasses. + + :param item: Completion item to format. + """ + raise NotImplementedError + + def complete(self) -> str: + """Produce the completion data to send back to the shell. + + By default this calls :meth:`get_completion_args`, gets the + completions, then calls :meth:`format_completion` for each + completion. + """ + args, incomplete = self.get_completion_args() + completions = self.get_completions(args, incomplete) + out = [self.format_completion(item) for item in completions] + return "\n".join(out) + + +class BashComplete(ShellComplete): + """Shell completion for Bash.""" + + name = "bash" + source_template = _SOURCE_BASH + + @staticmethod + def _check_version() -> None: + import shutil + import subprocess + + bash_exe = shutil.which("bash") + + if bash_exe is None: + match = None + else: + output = subprocess.run( + [bash_exe, "--norc", "-c", 'echo "${BASH_VERSION}"'], + stdout=subprocess.PIPE, + ) + match = re.search(r"^(\d+)\.(\d+)\.\d+", output.stdout.decode()) + + if match is not None: + major, minor = match.groups() + + if major < "4" or major == "4" and minor < "4": + echo( + _( + "Shell completion is not supported for Bash" + " versions older than 4.4." + ), + err=True, + ) + else: + echo( + _("Couldn't detect Bash version, shell completion is not supported."), + err=True, + ) + + def source(self) -> str: + self._check_version() + return super().source() + + def get_completion_args(self) -> tuple[list[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + return f"{item.type},{item.value}" + + +class ZshComplete(ShellComplete): + """Shell completion for Zsh.""" + + name = "zsh" + source_template = _SOURCE_ZSH + + def get_completion_args(self) -> tuple[list[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + cword = int(os.environ["COMP_CWORD"]) + args = cwords[1:cword] + + try: + incomplete = cwords[cword] + except IndexError: + incomplete = "" + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + help_ = item.help or "_" + # The zsh completion script uses `_describe` on items with help + # texts (which splits the item help from the item value at the + # first unescaped colon) and `compadd` on items without help + # text (which uses the item value as-is and does not support + # colon escaping). So escape colons in the item value if and + # only if the item help is not the sentinel "_" value, as used + # by the completion script. + # + # (The zsh completion script is potentially widely deployed, and + # thus harder to fix than this method.) + # + # See issue #1812 and issue #2703 for further context. + value = item.value.replace(":", r"\:") if help_ != "_" else item.value + return f"{item.type}\n{value}\n{help_}" + + +class FishComplete(ShellComplete): + """Shell completion for Fish.""" + + name = "fish" + source_template = _SOURCE_FISH + + def get_completion_args(self) -> tuple[list[str], str]: + cwords = split_arg_string(os.environ["COMP_WORDS"]) + incomplete = os.environ["COMP_CWORD"] + if incomplete: + incomplete = split_arg_string(incomplete)[0] + args = cwords[1:] + + # Fish stores the partial word in both COMP_WORDS and + # COMP_CWORD, remove it from complete args. + if incomplete and args and args[-1] == incomplete: + args.pop() + + return args, incomplete + + def format_completion(self, item: CompletionItem) -> str: + if item.help: + return f"{item.type},{item.value}\t{item.help}" + + return f"{item.type},{item.value}" + + +ShellCompleteType = t.TypeVar("ShellCompleteType", bound="type[ShellComplete]") + + +_available_shells: dict[str, type[ShellComplete]] = { + "bash": BashComplete, + "fish": FishComplete, + "zsh": ZshComplete, +} + + +def add_completion_class( + cls: ShellCompleteType, name: str | None = None +) -> ShellCompleteType: + """Register a :class:`ShellComplete` subclass under the given name. + The name will be provided by the completion instruction environment + variable during completion. + + :param cls: The completion class that will handle completion for the + shell. + :param name: Name to register the class under. Defaults to the + class's ``name`` attribute. + """ + if name is None: + name = cls.name + + _available_shells[name] = cls + + return cls + + +def get_completion_class(shell: str) -> type[ShellComplete] | None: + """Look up a registered :class:`ShellComplete` subclass by the name + provided by the completion instruction environment variable. If the + name isn't registered, returns ``None``. + + :param shell: Name the class is registered under. + """ + return _available_shells.get(shell) + + +def split_arg_string(string: str) -> list[str]: + """Split an argument string as with :func:`shlex.split`, but don't + fail if the string is incomplete. Ignores a missing closing quote or + incomplete escape sequence and uses the partial token as-is. + + .. code-block:: python + + split_arg_string("example 'my file") + ["example", "my file"] + + split_arg_string("example my\\") + ["example", "my"] + + :param string: String to split. + + .. versionchanged:: 8.2 + Moved to ``shell_completion`` from ``parser``. + """ + import shlex + + lex = shlex.shlex(string, posix=True) + lex.whitespace_split = True + lex.commenters = "" + out = [] + + try: + for token in lex: + out.append(token) + except ValueError: + # Raised when end-of-string is reached in an invalid state. Use + # the partial token as-is. The quote or escape character is in + # lex.state, not lex.token. + out.append(lex.token) + + return out + + +def _is_incomplete_argument(ctx: Context, param: Parameter) -> bool: + """Determine if the given parameter is an argument that can still + accept values. + + :param ctx: Invocation context for the command represented by the + parsed complete args. + :param param: Argument object being checked. + """ + if not isinstance(param, Argument): + return False + + assert param.name is not None + # Will be None if expose_value is False. + value = ctx.params.get(param.name) + return ( + param.nargs == -1 + or ctx.get_parameter_source(param.name) is not ParameterSource.COMMANDLINE + or ( + param.nargs > 1 + and isinstance(value, (tuple, list)) + and len(value) < param.nargs + ) + ) + + +def _start_of_option(ctx: Context, value: str) -> bool: + """Check if the value looks like the start of an option.""" + if not value: + return False + + c = value[0] + return c in ctx._opt_prefixes + + +def _is_incomplete_option(ctx: Context, args: list[str], param: Parameter) -> bool: + """Determine if the given parameter is an option that needs a value. + + :param args: List of complete args before the incomplete value. + :param param: Option object being checked. + """ + if not isinstance(param, Option): + return False + + if param.is_flag or param.count: + return False + + last_option = None + + for index, arg in enumerate(reversed(args)): + if index + 1 > param.nargs: + break + + if _start_of_option(ctx, arg): + last_option = arg + break + + return last_option is not None and last_option in param.opts + + +def _resolve_context( + cli: Command, + ctx_args: cabc.MutableMapping[str, t.Any], + prog_name: str, + args: list[str], +) -> Context: + """Produce the context hierarchy starting with the command and + traversing the complete arguments. This only follows the commands, + it doesn't trigger input prompts or callbacks. + + :param cli: Command being called. + :param prog_name: Name of the executable in the shell. + :param args: List of complete args before the incomplete value. + """ + ctx_args["resilient_parsing"] = True + with cli.make_context(prog_name, args.copy(), **ctx_args) as ctx: + args = ctx._protected_args + ctx.args + + while args: + command = ctx.command + + if isinstance(command, Group): + if not command.chain: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + with cmd.make_context( + name, args, parent=ctx, resilient_parsing=True + ) as sub_ctx: + ctx = sub_ctx + args = ctx._protected_args + ctx.args + else: + sub_ctx = ctx + + while args: + name, cmd, args = command.resolve_command(ctx, args) + + if cmd is None: + return ctx + + with cmd.make_context( + name, + args, + parent=ctx, + allow_extra_args=True, + allow_interspersed_args=False, + resilient_parsing=True, + ) as sub_sub_ctx: + sub_ctx = sub_sub_ctx + args = sub_ctx.args + + ctx = sub_ctx + args = [*sub_ctx._protected_args, *sub_ctx.args] + else: + break + + return ctx + + +def _resolve_incomplete( + ctx: Context, args: list[str], incomplete: str +) -> tuple[Command | Parameter, str]: + """Find the Click object that will handle the completion of the + incomplete value. Return the object and the incomplete value. + + :param ctx: Invocation context for the command represented by + the parsed complete args. + :param args: List of complete args before the incomplete value. + :param incomplete: Value being completed. May be empty. + """ + # Different shells treat an "=" between a long option name and + # value differently. Might keep the value joined, return the "=" + # as a separate item, or return the split name and value. Always + # split and discard the "=" to make completion easier. + if incomplete == "=": + incomplete = "" + elif "=" in incomplete and _start_of_option(ctx, incomplete): + name, _, incomplete = incomplete.partition("=") + args.append(name) + + # The "--" marker tells Click to stop treating values as options + # even if they start with the option character. If it hasn't been + # given and the incomplete arg looks like an option, the current + # command will provide option name completions. + if "--" not in args and _start_of_option(ctx, incomplete): + return ctx.command, incomplete + + params = ctx.command.get_params(ctx) + + # If the last complete arg is an option name with an incomplete + # value, the option will provide value completions. + for param in params: + if _is_incomplete_option(ctx, args, param): + return param, incomplete + + # It's not an option name or value. The first argument without a + # parsed value will provide value completions. + for param in params: + if _is_incomplete_argument(ctx, param): + return param, incomplete + + # There were no unparsed arguments, the command may be a group that + # will provide command name completions. + return ctx.command, incomplete diff --git a/netdeploy/lib/python3.11/site-packages/click/termui.py b/netdeploy/lib/python3.11/site-packages/click/termui.py new file mode 100644 index 0000000..dcbb222 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/termui.py @@ -0,0 +1,877 @@ +from __future__ import annotations + +import collections.abc as cabc +import inspect +import io +import itertools +import sys +import typing as t +from contextlib import AbstractContextManager +from gettext import gettext as _ + +from ._compat import isatty +from ._compat import strip_ansi +from .exceptions import Abort +from .exceptions import UsageError +from .globals import resolve_color_default +from .types import Choice +from .types import convert_type +from .types import ParamType +from .utils import echo +from .utils import LazyFile + +if t.TYPE_CHECKING: + from ._termui_impl import ProgressBar + +V = t.TypeVar("V") + +# The prompt functions to use. The doc tools currently override these +# functions to customize how they work. +visible_prompt_func: t.Callable[[str], str] = input + +_ansi_colors = { + "black": 30, + "red": 31, + "green": 32, + "yellow": 33, + "blue": 34, + "magenta": 35, + "cyan": 36, + "white": 37, + "reset": 39, + "bright_black": 90, + "bright_red": 91, + "bright_green": 92, + "bright_yellow": 93, + "bright_blue": 94, + "bright_magenta": 95, + "bright_cyan": 96, + "bright_white": 97, +} +_ansi_reset_all = "\033[0m" + + +def hidden_prompt_func(prompt: str) -> str: + import getpass + + return getpass.getpass(prompt) + + +def _build_prompt( + text: str, + suffix: str, + show_default: bool = False, + default: t.Any | None = None, + show_choices: bool = True, + type: ParamType | None = None, +) -> str: + prompt = text + if type is not None and show_choices and isinstance(type, Choice): + prompt += f" ({', '.join(map(str, type.choices))})" + if default is not None and show_default: + prompt = f"{prompt} [{_format_default(default)}]" + return f"{prompt}{suffix}" + + +def _format_default(default: t.Any) -> t.Any: + if isinstance(default, (io.IOBase, LazyFile)) and hasattr(default, "name"): + return default.name + + return default + + +def prompt( + text: str, + default: t.Any | None = None, + hide_input: bool = False, + confirmation_prompt: bool | str = False, + type: ParamType | t.Any | None = None, + value_proc: t.Callable[[str], t.Any] | None = None, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, + show_choices: bool = True, +) -> t.Any: + """Prompts a user for input. This is a convenience function that can + be used to prompt a user for input later. + + If the user aborts the input by sending an interrupt signal, this + function will catch it and raise a :exc:`Abort` exception. + + :param text: the text to show for the prompt. + :param default: the default value to use if no input happens. If this + is not given it will prompt until it's aborted. + :param hide_input: if this is set to true then the input value will + be hidden. + :param confirmation_prompt: Prompt a second time to confirm the + value. Can be set to a string instead of ``True`` to customize + the message. + :param type: the type to use to check the value against. + :param value_proc: if this parameter is provided it's a function that + is invoked instead of the type conversion to + convert a value. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + :param show_choices: Show or hide choices if the passed type is a Choice. + For example if type is a Choice of either day or week, + show_choices is true and text is "Group by" then the + prompt will be "Group by (day, week): ". + + .. versionadded:: 8.0 + ``confirmation_prompt`` can be a custom string. + + .. versionadded:: 7.0 + Added the ``show_choices`` parameter. + + .. versionadded:: 6.0 + Added unicode support for cmd.exe on Windows. + + .. versionadded:: 4.0 + Added the `err` parameter. + + """ + + def prompt_func(text: str) -> str: + f = hidden_prompt_func if hide_input else visible_prompt_func + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(text.rstrip(" "), nl=False, err=err) + # Echo a space to stdout to work around an issue where + # readline causes backspace to clear the whole line. + return f(" ") + except (KeyboardInterrupt, EOFError): + # getpass doesn't print a newline if the user aborts input with ^C. + # Allegedly this behavior is inherited from getpass(3). + # A doc bug has been filed at https://bugs.python.org/issue24711 + if hide_input: + echo(None, err=err) + raise Abort() from None + + if value_proc is None: + value_proc = convert_type(type, default) + + prompt = _build_prompt( + text, prompt_suffix, show_default, default, show_choices, type + ) + + if confirmation_prompt: + if confirmation_prompt is True: + confirmation_prompt = _("Repeat for confirmation") + + confirmation_prompt = _build_prompt(confirmation_prompt, prompt_suffix) + + while True: + while True: + value = prompt_func(prompt) + if value: + break + elif default is not None: + value = default + break + try: + result = value_proc(value) + except UsageError as e: + if hide_input: + echo(_("Error: The value you entered was invalid."), err=err) + else: + echo(_("Error: {e.message}").format(e=e), err=err) + continue + if not confirmation_prompt: + return result + while True: + value2 = prompt_func(confirmation_prompt) + is_empty = not value and not value2 + if value2 or is_empty: + break + if value == value2: + return result + echo(_("Error: The two entered values do not match."), err=err) + + +def confirm( + text: str, + default: bool | None = False, + abort: bool = False, + prompt_suffix: str = ": ", + show_default: bool = True, + err: bool = False, +) -> bool: + """Prompts for confirmation (yes/no question). + + If the user aborts the input by sending a interrupt signal this + function will catch it and raise a :exc:`Abort` exception. + + :param text: the question to ask. + :param default: The default value to use when no input is given. If + ``None``, repeat until input is given. + :param abort: if this is set to `True` a negative answer aborts the + exception by raising :exc:`Abort`. + :param prompt_suffix: a suffix that should be added to the prompt. + :param show_default: shows or hides the default value in the prompt. + :param err: if set to true the file defaults to ``stderr`` instead of + ``stdout``, the same as with echo. + + .. versionchanged:: 8.0 + Repeat until input is given if ``default`` is ``None``. + + .. versionadded:: 4.0 + Added the ``err`` parameter. + """ + prompt = _build_prompt( + text, + prompt_suffix, + show_default, + "y/n" if default is None else ("Y/n" if default else "y/N"), + ) + + while True: + try: + # Write the prompt separately so that we get nice + # coloring through colorama on Windows + echo(prompt.rstrip(" "), nl=False, err=err) + # Echo a space to stdout to work around an issue where + # readline causes backspace to clear the whole line. + value = visible_prompt_func(" ").lower().strip() + except (KeyboardInterrupt, EOFError): + raise Abort() from None + if value in ("y", "yes"): + rv = True + elif value in ("n", "no"): + rv = False + elif default is not None and value == "": + rv = default + else: + echo(_("Error: invalid input"), err=err) + continue + break + if abort and not rv: + raise Abort() + return rv + + +def echo_via_pager( + text_or_generator: cabc.Iterable[str] | t.Callable[[], cabc.Iterable[str]] | str, + color: bool | None = None, +) -> None: + """This function takes a text and shows it via an environment specific + pager on stdout. + + .. versionchanged:: 3.0 + Added the `color` flag. + + :param text_or_generator: the text to page, or alternatively, a + generator emitting the text to page. + :param color: controls if the pager supports ANSI colors or not. The + default is autodetection. + """ + color = resolve_color_default(color) + + if inspect.isgeneratorfunction(text_or_generator): + i = t.cast("t.Callable[[], cabc.Iterable[str]]", text_or_generator)() + elif isinstance(text_or_generator, str): + i = [text_or_generator] + else: + i = iter(t.cast("cabc.Iterable[str]", text_or_generator)) + + # convert every element of i to a text type if necessary + text_generator = (el if isinstance(el, str) else str(el) for el in i) + + from ._termui_impl import pager + + return pager(itertools.chain(text_generator, "\n"), color) + + +@t.overload +def progressbar( + *, + length: int, + label: str | None = None, + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, +) -> ProgressBar[int]: ... + + +@t.overload +def progressbar( + iterable: cabc.Iterable[V] | None = None, + length: int | None = None, + label: str | None = None, + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + item_show_func: t.Callable[[V | None], str | None] | None = None, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, +) -> ProgressBar[V]: ... + + +def progressbar( + iterable: cabc.Iterable[V] | None = None, + length: int | None = None, + label: str | None = None, + hidden: bool = False, + show_eta: bool = True, + show_percent: bool | None = None, + show_pos: bool = False, + item_show_func: t.Callable[[V | None], str | None] | None = None, + fill_char: str = "#", + empty_char: str = "-", + bar_template: str = "%(label)s [%(bar)s] %(info)s", + info_sep: str = " ", + width: int = 36, + file: t.TextIO | None = None, + color: bool | None = None, + update_min_steps: int = 1, +) -> ProgressBar[V]: + """This function creates an iterable context manager that can be used + to iterate over something while showing a progress bar. It will + either iterate over the `iterable` or `length` items (that are counted + up). While iteration happens, this function will print a rendered + progress bar to the given `file` (defaults to stdout) and will attempt + to calculate remaining time and more. By default, this progress bar + will not be rendered if the file is not a terminal. + + The context manager creates the progress bar. When the context + manager is entered the progress bar is already created. With every + iteration over the progress bar, the iterable passed to the bar is + advanced and the bar is updated. When the context manager exits, + a newline is printed and the progress bar is finalized on screen. + + Note: The progress bar is currently designed for use cases where the + total progress can be expected to take at least several seconds. + Because of this, the ProgressBar class object won't display + progress that is considered too fast, and progress where the time + between steps is less than a second. + + No printing must happen or the progress bar will be unintentionally + destroyed. + + Example usage:: + + with progressbar(items) as bar: + for item in bar: + do_something_with(item) + + Alternatively, if no iterable is specified, one can manually update the + progress bar through the `update()` method instead of directly + iterating over the progress bar. The update method accepts the number + of steps to increment the bar with:: + + with progressbar(length=chunks.total_bytes) as bar: + for chunk in chunks: + process_chunk(chunk) + bar.update(chunks.bytes) + + The ``update()`` method also takes an optional value specifying the + ``current_item`` at the new position. This is useful when used + together with ``item_show_func`` to customize the output for each + manual step:: + + with click.progressbar( + length=total_size, + label='Unzipping archive', + item_show_func=lambda a: a.filename + ) as bar: + for archive in zip_file: + archive.extract() + bar.update(archive.size, archive) + + :param iterable: an iterable to iterate over. If not provided the length + is required. + :param length: the number of items to iterate over. By default the + progressbar will attempt to ask the iterator about its + length, which might or might not work. If an iterable is + also provided this parameter can be used to override the + length. If an iterable is not provided the progress bar + will iterate over a range of that length. + :param label: the label to show next to the progress bar. + :param hidden: hide the progressbar. Defaults to ``False``. When no tty is + detected, it will only print the progressbar label. Setting this to + ``False`` also disables that. + :param show_eta: enables or disables the estimated time display. This is + automatically disabled if the length cannot be + determined. + :param show_percent: enables or disables the percentage display. The + default is `True` if the iterable has a length or + `False` if not. + :param show_pos: enables or disables the absolute position display. The + default is `False`. + :param item_show_func: A function called with the current item which + can return a string to show next to the progress bar. If the + function returns ``None`` nothing is shown. The current item can + be ``None``, such as when entering and exiting the bar. + :param fill_char: the character to use to show the filled part of the + progress bar. + :param empty_char: the character to use to show the non-filled part of + the progress bar. + :param bar_template: the format string to use as template for the bar. + The parameters in it are ``label`` for the label, + ``bar`` for the progress bar and ``info`` for the + info section. + :param info_sep: the separator between multiple info items (eta etc.) + :param width: the width of the progress bar in characters, 0 means full + terminal width + :param file: The file to write to. If this is not a terminal then + only the label is printed. + :param color: controls if the terminal supports ANSI colors or not. The + default is autodetection. This is only needed if ANSI + codes are included anywhere in the progress bar output + which is not the case by default. + :param update_min_steps: Render only when this many updates have + completed. This allows tuning for very fast iterators. + + .. versionadded:: 8.2 + The ``hidden`` argument. + + .. versionchanged:: 8.0 + Output is shown even if execution time is less than 0.5 seconds. + + .. versionchanged:: 8.0 + ``item_show_func`` shows the current item, not the previous one. + + .. versionchanged:: 8.0 + Labels are echoed if the output is not a TTY. Reverts a change + in 7.0 that removed all output. + + .. versionadded:: 8.0 + The ``update_min_steps`` parameter. + + .. versionadded:: 4.0 + The ``color`` parameter and ``update`` method. + + .. versionadded:: 2.0 + """ + from ._termui_impl import ProgressBar + + color = resolve_color_default(color) + return ProgressBar( + iterable=iterable, + length=length, + hidden=hidden, + show_eta=show_eta, + show_percent=show_percent, + show_pos=show_pos, + item_show_func=item_show_func, + fill_char=fill_char, + empty_char=empty_char, + bar_template=bar_template, + info_sep=info_sep, + file=file, + label=label, + width=width, + color=color, + update_min_steps=update_min_steps, + ) + + +def clear() -> None: + """Clears the terminal screen. This will have the effect of clearing + the whole visible space of the terminal and moving the cursor to the + top left. This does not do anything if not connected to a terminal. + + .. versionadded:: 2.0 + """ + if not isatty(sys.stdout): + return + + # ANSI escape \033[2J clears the screen, \033[1;1H moves the cursor + echo("\033[2J\033[1;1H", nl=False) + + +def _interpret_color(color: int | tuple[int, int, int] | str, offset: int = 0) -> str: + if isinstance(color, int): + return f"{38 + offset};5;{color:d}" + + if isinstance(color, (tuple, list)): + r, g, b = color + return f"{38 + offset};2;{r:d};{g:d};{b:d}" + + return str(_ansi_colors[color] + offset) + + +def style( + text: t.Any, + fg: int | tuple[int, int, int] | str | None = None, + bg: int | tuple[int, int, int] | str | None = None, + bold: bool | None = None, + dim: bool | None = None, + underline: bool | None = None, + overline: bool | None = None, + italic: bool | None = None, + blink: bool | None = None, + reverse: bool | None = None, + strikethrough: bool | None = None, + reset: bool = True, +) -> str: + """Styles a text with ANSI styles and returns the new string. By + default the styling is self contained which means that at the end + of the string a reset code is issued. This can be prevented by + passing ``reset=False``. + + Examples:: + + click.echo(click.style('Hello World!', fg='green')) + click.echo(click.style('ATTENTION!', blink=True)) + click.echo(click.style('Some things', reverse=True, fg='cyan')) + click.echo(click.style('More colors', fg=(255, 12, 128), bg=117)) + + Supported color names: + + * ``black`` (might be a gray) + * ``red`` + * ``green`` + * ``yellow`` (might be an orange) + * ``blue`` + * ``magenta`` + * ``cyan`` + * ``white`` (might be light gray) + * ``bright_black`` + * ``bright_red`` + * ``bright_green`` + * ``bright_yellow`` + * ``bright_blue`` + * ``bright_magenta`` + * ``bright_cyan`` + * ``bright_white`` + * ``reset`` (reset the color code only) + + If the terminal supports it, color may also be specified as: + + - An integer in the interval [0, 255]. The terminal must support + 8-bit/256-color mode. + - An RGB tuple of three integers in [0, 255]. The terminal must + support 24-bit/true-color mode. + + See https://en.wikipedia.org/wiki/ANSI_color and + https://gist.github.com/XVilka/8346728 for more information. + + :param text: the string to style with ansi codes. + :param fg: if provided this will become the foreground color. + :param bg: if provided this will become the background color. + :param bold: if provided this will enable or disable bold mode. + :param dim: if provided this will enable or disable dim mode. This is + badly supported. + :param underline: if provided this will enable or disable underline. + :param overline: if provided this will enable or disable overline. + :param italic: if provided this will enable or disable italic. + :param blink: if provided this will enable or disable blinking. + :param reverse: if provided this will enable or disable inverse + rendering (foreground becomes background and the + other way round). + :param strikethrough: if provided this will enable or disable + striking through text. + :param reset: by default a reset-all code is added at the end of the + string which means that styles do not carry over. This + can be disabled to compose styles. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. + + .. versionchanged:: 8.0 + Added support for 256 and RGB color codes. + + .. versionchanged:: 8.0 + Added the ``strikethrough``, ``italic``, and ``overline`` + parameters. + + .. versionchanged:: 7.0 + Added support for bright colors. + + .. versionadded:: 2.0 + """ + if not isinstance(text, str): + text = str(text) + + bits = [] + + if fg: + try: + bits.append(f"\033[{_interpret_color(fg)}m") + except KeyError: + raise TypeError(f"Unknown color {fg!r}") from None + + if bg: + try: + bits.append(f"\033[{_interpret_color(bg, 10)}m") + except KeyError: + raise TypeError(f"Unknown color {bg!r}") from None + + if bold is not None: + bits.append(f"\033[{1 if bold else 22}m") + if dim is not None: + bits.append(f"\033[{2 if dim else 22}m") + if underline is not None: + bits.append(f"\033[{4 if underline else 24}m") + if overline is not None: + bits.append(f"\033[{53 if overline else 55}m") + if italic is not None: + bits.append(f"\033[{3 if italic else 23}m") + if blink is not None: + bits.append(f"\033[{5 if blink else 25}m") + if reverse is not None: + bits.append(f"\033[{7 if reverse else 27}m") + if strikethrough is not None: + bits.append(f"\033[{9 if strikethrough else 29}m") + bits.append(text) + if reset: + bits.append(_ansi_reset_all) + return "".join(bits) + + +def unstyle(text: str) -> str: + """Removes ANSI styling information from a string. Usually it's not + necessary to use this function as Click's echo function will + automatically remove styling if necessary. + + .. versionadded:: 2.0 + + :param text: the text to remove style information from. + """ + return strip_ansi(text) + + +def secho( + message: t.Any | None = None, + file: t.IO[t.AnyStr] | None = None, + nl: bool = True, + err: bool = False, + color: bool | None = None, + **styles: t.Any, +) -> None: + """This function combines :func:`echo` and :func:`style` into one + call. As such the following two calls are the same:: + + click.secho('Hello World!', fg='green') + click.echo(click.style('Hello World!', fg='green')) + + All keyword arguments are forwarded to the underlying functions + depending on which one they go with. + + Non-string types will be converted to :class:`str`. However, + :class:`bytes` are passed directly to :meth:`echo` without applying + style. If you want to style bytes that represent text, call + :meth:`bytes.decode` first. + + .. versionchanged:: 8.0 + A non-string ``message`` is converted to a string. Bytes are + passed through without style applied. + + .. versionadded:: 2.0 + """ + if message is not None and not isinstance(message, (bytes, bytearray)): + message = style(message, **styles) + + return echo(message, file=file, nl=nl, err=err, color=color) + + +@t.overload +def edit( + text: bytes | bytearray, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = False, + extension: str = ".txt", +) -> bytes | None: ... + + +@t.overload +def edit( + text: str, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", +) -> str | None: ... + + +@t.overload +def edit( + text: None = None, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", + filename: str | cabc.Iterable[str] | None = None, +) -> None: ... + + +def edit( + text: str | bytes | bytearray | None = None, + editor: str | None = None, + env: cabc.Mapping[str, str] | None = None, + require_save: bool = True, + extension: str = ".txt", + filename: str | cabc.Iterable[str] | None = None, +) -> str | bytes | bytearray | None: + r"""Edits the given text in the defined editor. If an editor is given + (should be the full path to the executable but the regular operating + system search path is used for finding the executable) it overrides + the detected editor. Optionally, some environment variables can be + used. If the editor is closed without changes, `None` is returned. In + case a file is edited directly the return value is always `None` and + `require_save` and `extension` are ignored. + + If the editor cannot be opened a :exc:`UsageError` is raised. + + Note for Windows: to simplify cross-platform usage, the newlines are + automatically converted from POSIX to Windows and vice versa. As such, + the message here will have ``\n`` as newline markers. + + :param text: the text to edit. + :param editor: optionally the editor to use. Defaults to automatic + detection. + :param env: environment variables to forward to the editor. + :param require_save: if this is true, then not saving in the editor + will make the return value become `None`. + :param extension: the extension to tell the editor about. This defaults + to `.txt` but changing this might change syntax + highlighting. + :param filename: if provided it will edit this file instead of the + provided text contents. It will not use a temporary + file as an indirection in that case. If the editor supports + editing multiple files at once, a sequence of files may be + passed as well. Invoke `click.file` once per file instead + if multiple files cannot be managed at once or editing the + files serially is desired. + + .. versionchanged:: 8.2.0 + ``filename`` now accepts any ``Iterable[str]`` in addition to a ``str`` + if the ``editor`` supports editing multiple files at once. + + """ + from ._termui_impl import Editor + + ed = Editor(editor=editor, env=env, require_save=require_save, extension=extension) + + if filename is None: + return ed.edit(text) + + if isinstance(filename, str): + filename = (filename,) + + ed.edit_files(filenames=filename) + return None + + +def launch(url: str, wait: bool = False, locate: bool = False) -> int: + """This function launches the given URL (or filename) in the default + viewer application for this file type. If this is an executable, it + might launch the executable in a new session. The return value is + the exit code of the launched application. Usually, ``0`` indicates + success. + + Examples:: + + click.launch('https://click.palletsprojects.com/') + click.launch('/my/downloaded/file', locate=True) + + .. versionadded:: 2.0 + + :param url: URL or filename of the thing to launch. + :param wait: Wait for the program to exit before returning. This + only works if the launched program blocks. In particular, + ``xdg-open`` on Linux does not block. + :param locate: if this is set to `True` then instead of launching the + application associated with the URL it will attempt to + launch a file manager with the file located. This + might have weird effects if the URL does not point to + the filesystem. + """ + from ._termui_impl import open_url + + return open_url(url, wait=wait, locate=locate) + + +# If this is provided, getchar() calls into this instead. This is used +# for unittesting purposes. +_getchar: t.Callable[[bool], str] | None = None + + +def getchar(echo: bool = False) -> str: + """Fetches a single character from the terminal and returns it. This + will always return a unicode character and under certain rare + circumstances this might return more than one character. The + situations which more than one character is returned is when for + whatever reason multiple characters end up in the terminal buffer or + standard input was not actually a terminal. + + Note that this will always read from the terminal, even if something + is piped into the standard input. + + Note for Windows: in rare cases when typing non-ASCII characters, this + function might wait for a second character and then return both at once. + This is because certain Unicode characters look like special-key markers. + + .. versionadded:: 2.0 + + :param echo: if set to `True`, the character read will also show up on + the terminal. The default is to not show it. + """ + global _getchar + + if _getchar is None: + from ._termui_impl import getchar as f + + _getchar = f + + return _getchar(echo) + + +def raw_terminal() -> AbstractContextManager[int]: + from ._termui_impl import raw_terminal as f + + return f() + + +def pause(info: str | None = None, err: bool = False) -> None: + """This command stops execution and waits for the user to press any + key to continue. This is similar to the Windows batch "pause" + command. If the program is not run through a terminal, this command + will instead do nothing. + + .. versionadded:: 2.0 + + .. versionadded:: 4.0 + Added the `err` parameter. + + :param info: The message to print before pausing. Defaults to + ``"Press any key to continue..."``. + :param err: if set to message goes to ``stderr`` instead of + ``stdout``, the same as with echo. + """ + if not isatty(sys.stdin) or not isatty(sys.stdout): + return + + if info is None: + info = _("Press any key to continue...") + + try: + if info: + echo(info, nl=False, err=err) + try: + getchar() + except (KeyboardInterrupt, EOFError): + pass + finally: + if info: + echo(err=err) diff --git a/netdeploy/lib/python3.11/site-packages/click/testing.py b/netdeploy/lib/python3.11/site-packages/click/testing.py new file mode 100644 index 0000000..f6f60b8 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/testing.py @@ -0,0 +1,577 @@ +from __future__ import annotations + +import collections.abc as cabc +import contextlib +import io +import os +import shlex +import sys +import tempfile +import typing as t +from types import TracebackType + +from . import _compat +from . import formatting +from . import termui +from . import utils +from ._compat import _find_binary_reader + +if t.TYPE_CHECKING: + from _typeshed import ReadableBuffer + + from .core import Command + + +class EchoingStdin: + def __init__(self, input: t.BinaryIO, output: t.BinaryIO) -> None: + self._input = input + self._output = output + self._paused = False + + def __getattr__(self, x: str) -> t.Any: + return getattr(self._input, x) + + def _echo(self, rv: bytes) -> bytes: + if not self._paused: + self._output.write(rv) + + return rv + + def read(self, n: int = -1) -> bytes: + return self._echo(self._input.read(n)) + + def read1(self, n: int = -1) -> bytes: + return self._echo(self._input.read1(n)) # type: ignore + + def readline(self, n: int = -1) -> bytes: + return self._echo(self._input.readline(n)) + + def readlines(self) -> list[bytes]: + return [self._echo(x) for x in self._input.readlines()] + + def __iter__(self) -> cabc.Iterator[bytes]: + return iter(self._echo(x) for x in self._input) + + def __repr__(self) -> str: + return repr(self._input) + + +@contextlib.contextmanager +def _pause_echo(stream: EchoingStdin | None) -> cabc.Iterator[None]: + if stream is None: + yield + else: + stream._paused = True + yield + stream._paused = False + + +class BytesIOCopy(io.BytesIO): + """Patch ``io.BytesIO`` to let the written stream be copied to another. + + .. versionadded:: 8.2 + """ + + def __init__(self, copy_to: io.BytesIO) -> None: + super().__init__() + self.copy_to = copy_to + + def flush(self) -> None: + super().flush() + self.copy_to.flush() + + def write(self, b: ReadableBuffer) -> int: + self.copy_to.write(b) + return super().write(b) + + +class StreamMixer: + """Mixes `` and `` streams. + + The result is available in the ``output`` attribute. + + .. versionadded:: 8.2 + """ + + def __init__(self) -> None: + self.output: io.BytesIO = io.BytesIO() + self.stdout: io.BytesIO = BytesIOCopy(copy_to=self.output) + self.stderr: io.BytesIO = BytesIOCopy(copy_to=self.output) + + def __del__(self) -> None: + """ + Guarantee that embedded file-like objects are closed in a + predictable order, protecting against races between + self.output being closed and other streams being flushed on close + + .. versionadded:: 8.2.2 + """ + self.stderr.close() + self.stdout.close() + self.output.close() + + +class _NamedTextIOWrapper(io.TextIOWrapper): + def __init__( + self, buffer: t.BinaryIO, name: str, mode: str, **kwargs: t.Any + ) -> None: + super().__init__(buffer, **kwargs) + self._name = name + self._mode = mode + + @property + def name(self) -> str: + return self._name + + @property + def mode(self) -> str: + return self._mode + + +def make_input_stream( + input: str | bytes | t.IO[t.Any] | None, charset: str +) -> t.BinaryIO: + # Is already an input stream. + if hasattr(input, "read"): + rv = _find_binary_reader(t.cast("t.IO[t.Any]", input)) + + if rv is not None: + return rv + + raise TypeError("Could not find binary reader for input stream.") + + if input is None: + input = b"" + elif isinstance(input, str): + input = input.encode(charset) + + return io.BytesIO(input) + + +class Result: + """Holds the captured result of an invoked CLI script. + + :param runner: The runner that created the result + :param stdout_bytes: The standard output as bytes. + :param stderr_bytes: The standard error as bytes. + :param output_bytes: A mix of ``stdout_bytes`` and ``stderr_bytes``, as the + user would see it in its terminal. + :param return_value: The value returned from the invoked command. + :param exit_code: The exit code as integer. + :param exception: The exception that happened if one did. + :param exc_info: Exception information (exception type, exception instance, + traceback type). + + .. versionchanged:: 8.2 + ``stderr_bytes`` no longer optional, ``output_bytes`` introduced and + ``mix_stderr`` has been removed. + + .. versionadded:: 8.0 + Added ``return_value``. + """ + + def __init__( + self, + runner: CliRunner, + stdout_bytes: bytes, + stderr_bytes: bytes, + output_bytes: bytes, + return_value: t.Any, + exit_code: int, + exception: BaseException | None, + exc_info: tuple[type[BaseException], BaseException, TracebackType] + | None = None, + ): + self.runner = runner + self.stdout_bytes = stdout_bytes + self.stderr_bytes = stderr_bytes + self.output_bytes = output_bytes + self.return_value = return_value + self.exit_code = exit_code + self.exception = exception + self.exc_info = exc_info + + @property + def output(self) -> str: + """The terminal output as unicode string, as the user would see it. + + .. versionchanged:: 8.2 + No longer a proxy for ``self.stdout``. Now has its own independent stream + that is mixing `` and ``, in the order they were written. + """ + return self.output_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + @property + def stdout(self) -> str: + """The standard output as unicode string.""" + return self.stdout_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + @property + def stderr(self) -> str: + """The standard error as unicode string. + + .. versionchanged:: 8.2 + No longer raise an exception, always returns the `` string. + """ + return self.stderr_bytes.decode(self.runner.charset, "replace").replace( + "\r\n", "\n" + ) + + def __repr__(self) -> str: + exc_str = repr(self.exception) if self.exception else "okay" + return f"<{type(self).__name__} {exc_str}>" + + +class CliRunner: + """The CLI runner provides functionality to invoke a Click command line + script for unittesting purposes in a isolated environment. This only + works in single-threaded systems without any concurrency as it changes the + global interpreter state. + + :param charset: the character set for the input and output data. + :param env: a dictionary with environment variables for overriding. + :param echo_stdin: if this is set to `True`, then reading from `` writes + to ``. This is useful for showing examples in + some circumstances. Note that regular prompts + will automatically echo the input. + :param catch_exceptions: Whether to catch any exceptions other than + ``SystemExit`` when running :meth:`~CliRunner.invoke`. + + .. versionchanged:: 8.2 + Added the ``catch_exceptions`` parameter. + + .. versionchanged:: 8.2 + ``mix_stderr`` parameter has been removed. + """ + + def __init__( + self, + charset: str = "utf-8", + env: cabc.Mapping[str, str | None] | None = None, + echo_stdin: bool = False, + catch_exceptions: bool = True, + ) -> None: + self.charset = charset + self.env: cabc.Mapping[str, str | None] = env or {} + self.echo_stdin = echo_stdin + self.catch_exceptions = catch_exceptions + + def get_default_prog_name(self, cli: Command) -> str: + """Given a command object it will return the default program name + for it. The default is the `name` attribute or ``"root"`` if not + set. + """ + return cli.name or "root" + + def make_env( + self, overrides: cabc.Mapping[str, str | None] | None = None + ) -> cabc.Mapping[str, str | None]: + """Returns the environment overrides for invoking a script.""" + rv = dict(self.env) + if overrides: + rv.update(overrides) + return rv + + @contextlib.contextmanager + def isolation( + self, + input: str | bytes | t.IO[t.Any] | None = None, + env: cabc.Mapping[str, str | None] | None = None, + color: bool = False, + ) -> cabc.Iterator[tuple[io.BytesIO, io.BytesIO, io.BytesIO]]: + """A context manager that sets up the isolation for invoking of a + command line tool. This sets up `` with the given input data + and `os.environ` with the overrides from the given dictionary. + This also rebinds some internals in Click to be mocked (like the + prompt functionality). + + This is automatically done in the :meth:`invoke` method. + + :param input: the input stream to put into `sys.stdin`. + :param env: the environment overrides as dictionary. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + + .. versionadded:: 8.2 + An additional output stream is returned, which is a mix of + `` and `` streams. + + .. versionchanged:: 8.2 + Always returns the `` stream. + + .. versionchanged:: 8.0 + `` is opened with ``errors="backslashreplace"`` + instead of the default ``"strict"``. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + """ + bytes_input = make_input_stream(input, self.charset) + echo_input = None + + old_stdin = sys.stdin + old_stdout = sys.stdout + old_stderr = sys.stderr + old_forced_width = formatting.FORCED_WIDTH + formatting.FORCED_WIDTH = 80 + + env = self.make_env(env) + + stream_mixer = StreamMixer() + + if self.echo_stdin: + bytes_input = echo_input = t.cast( + t.BinaryIO, EchoingStdin(bytes_input, stream_mixer.stdout) + ) + + sys.stdin = text_input = _NamedTextIOWrapper( + bytes_input, encoding=self.charset, name="", mode="r" + ) + + if self.echo_stdin: + # Force unbuffered reads, otherwise TextIOWrapper reads a + # large chunk which is echoed early. + text_input._CHUNK_SIZE = 1 # type: ignore + + sys.stdout = _NamedTextIOWrapper( + stream_mixer.stdout, encoding=self.charset, name="", mode="w" + ) + + sys.stderr = _NamedTextIOWrapper( + stream_mixer.stderr, + encoding=self.charset, + name="", + mode="w", + errors="backslashreplace", + ) + + @_pause_echo(echo_input) # type: ignore + def visible_input(prompt: str | None = None) -> str: + sys.stdout.write(prompt or "") + try: + val = next(text_input).rstrip("\r\n") + except StopIteration as e: + raise EOFError() from e + sys.stdout.write(f"{val}\n") + sys.stdout.flush() + return val + + @_pause_echo(echo_input) # type: ignore + def hidden_input(prompt: str | None = None) -> str: + sys.stdout.write(f"{prompt or ''}\n") + sys.stdout.flush() + try: + return next(text_input).rstrip("\r\n") + except StopIteration as e: + raise EOFError() from e + + @_pause_echo(echo_input) # type: ignore + def _getchar(echo: bool) -> str: + char = sys.stdin.read(1) + + if echo: + sys.stdout.write(char) + + sys.stdout.flush() + return char + + default_color = color + + def should_strip_ansi( + stream: t.IO[t.Any] | None = None, color: bool | None = None + ) -> bool: + if color is None: + return not default_color + return not color + + old_visible_prompt_func = termui.visible_prompt_func + old_hidden_prompt_func = termui.hidden_prompt_func + old__getchar_func = termui._getchar + old_should_strip_ansi = utils.should_strip_ansi # type: ignore + old__compat_should_strip_ansi = _compat.should_strip_ansi + termui.visible_prompt_func = visible_input + termui.hidden_prompt_func = hidden_input + termui._getchar = _getchar + utils.should_strip_ansi = should_strip_ansi # type: ignore + _compat.should_strip_ansi = should_strip_ansi + + old_env = {} + try: + for key, value in env.items(): + old_env[key] = os.environ.get(key) + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + yield (stream_mixer.stdout, stream_mixer.stderr, stream_mixer.output) + finally: + for key, value in old_env.items(): + if value is None: + try: + del os.environ[key] + except Exception: + pass + else: + os.environ[key] = value + sys.stdout = old_stdout + sys.stderr = old_stderr + sys.stdin = old_stdin + termui.visible_prompt_func = old_visible_prompt_func + termui.hidden_prompt_func = old_hidden_prompt_func + termui._getchar = old__getchar_func + utils.should_strip_ansi = old_should_strip_ansi # type: ignore + _compat.should_strip_ansi = old__compat_should_strip_ansi + formatting.FORCED_WIDTH = old_forced_width + + def invoke( + self, + cli: Command, + args: str | cabc.Sequence[str] | None = None, + input: str | bytes | t.IO[t.Any] | None = None, + env: cabc.Mapping[str, str | None] | None = None, + catch_exceptions: bool | None = None, + color: bool = False, + **extra: t.Any, + ) -> Result: + """Invokes a command in an isolated environment. The arguments are + forwarded directly to the command line script, the `extra` keyword + arguments are passed to the :meth:`~clickpkg.Command.main` function of + the command. + + This returns a :class:`Result` object. + + :param cli: the command to invoke + :param args: the arguments to invoke. It may be given as an iterable + or a string. When given as string it will be interpreted + as a Unix shell command. More details at + :func:`shlex.split`. + :param input: the input data for `sys.stdin`. + :param env: the environment overrides. + :param catch_exceptions: Whether to catch any other exceptions than + ``SystemExit``. If :data:`None`, the value + from :class:`CliRunner` is used. + :param extra: the keyword arguments to pass to :meth:`main`. + :param color: whether the output should contain color codes. The + application can still override this explicitly. + + .. versionadded:: 8.2 + The result object has the ``output_bytes`` attribute with + the mix of ``stdout_bytes`` and ``stderr_bytes``, as the user would + see it in its terminal. + + .. versionchanged:: 8.2 + The result object always returns the ``stderr_bytes`` stream. + + .. versionchanged:: 8.0 + The result object has the ``return_value`` attribute with + the value returned from the invoked command. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionchanged:: 3.0 + Added the ``catch_exceptions`` parameter. + + .. versionchanged:: 3.0 + The result object has the ``exc_info`` attribute with the + traceback if available. + """ + exc_info = None + if catch_exceptions is None: + catch_exceptions = self.catch_exceptions + + with self.isolation(input=input, env=env, color=color) as outstreams: + return_value = None + exception: BaseException | None = None + exit_code = 0 + + if isinstance(args, str): + args = shlex.split(args) + + try: + prog_name = extra.pop("prog_name") + except KeyError: + prog_name = self.get_default_prog_name(cli) + + try: + return_value = cli.main(args=args or (), prog_name=prog_name, **extra) + except SystemExit as e: + exc_info = sys.exc_info() + e_code = t.cast("int | t.Any | None", e.code) + + if e_code is None: + e_code = 0 + + if e_code != 0: + exception = e + + if not isinstance(e_code, int): + sys.stdout.write(str(e_code)) + sys.stdout.write("\n") + e_code = 1 + + exit_code = e_code + + except Exception as e: + if not catch_exceptions: + raise + exception = e + exit_code = 1 + exc_info = sys.exc_info() + finally: + sys.stdout.flush() + sys.stderr.flush() + stdout = outstreams[0].getvalue() + stderr = outstreams[1].getvalue() + output = outstreams[2].getvalue() + + return Result( + runner=self, + stdout_bytes=stdout, + stderr_bytes=stderr, + output_bytes=output, + return_value=return_value, + exit_code=exit_code, + exception=exception, + exc_info=exc_info, # type: ignore + ) + + @contextlib.contextmanager + def isolated_filesystem( + self, temp_dir: str | os.PathLike[str] | None = None + ) -> cabc.Iterator[str]: + """A context manager that creates a temporary directory and + changes the current working directory to it. This isolates tests + that affect the contents of the CWD to prevent them from + interfering with each other. + + :param temp_dir: Create the temporary directory under this + directory. If given, the created directory is not removed + when exiting. + + .. versionchanged:: 8.0 + Added the ``temp_dir`` parameter. + """ + cwd = os.getcwd() + dt = tempfile.mkdtemp(dir=temp_dir) + os.chdir(dt) + + try: + yield dt + finally: + os.chdir(cwd) + + if temp_dir is None: + import shutil + + try: + shutil.rmtree(dt) + except OSError: + pass diff --git a/netdeploy/lib/python3.11/site-packages/click/types.py b/netdeploy/lib/python3.11/site-packages/click/types.py new file mode 100644 index 0000000..e71c1c2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/types.py @@ -0,0 +1,1209 @@ +from __future__ import annotations + +import collections.abc as cabc +import enum +import os +import stat +import sys +import typing as t +from datetime import datetime +from gettext import gettext as _ +from gettext import ngettext + +from ._compat import _get_argv_encoding +from ._compat import open_stream +from .exceptions import BadParameter +from .utils import format_filename +from .utils import LazyFile +from .utils import safecall + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .core import Context + from .core import Parameter + from .shell_completion import CompletionItem + +ParamTypeValue = t.TypeVar("ParamTypeValue") + + +class ParamType: + """Represents the type of a parameter. Validates and converts values + from the command line or Python into the correct type. + + To implement a custom type, subclass and implement at least the + following: + + - The :attr:`name` class attribute must be set. + - Calling an instance of the type with ``None`` must return + ``None``. This is already implemented by default. + - :meth:`convert` must convert string values to the correct type. + - :meth:`convert` must accept values that are already the correct + type. + - It must be able to convert a value if the ``ctx`` and ``param`` + arguments are ``None``. This can occur when converting prompt + input. + """ + + is_composite: t.ClassVar[bool] = False + arity: t.ClassVar[int] = 1 + + #: the descriptive name of this type + name: str + + #: if a list of this type is expected and the value is pulled from a + #: string environment variable, this is what splits it up. `None` + #: means any whitespace. For all parameters the general rule is that + #: whitespace splits them up. The exception are paths and files which + #: are split by ``os.path.pathsep`` by default (":" on Unix and ";" on + #: Windows). + envvar_list_splitter: t.ClassVar[str | None] = None + + def to_info_dict(self) -> dict[str, t.Any]: + """Gather information that could be useful for a tool generating + user-facing documentation. + + Use :meth:`click.Context.to_info_dict` to traverse the entire + CLI structure. + + .. versionadded:: 8.0 + """ + # The class name without the "ParamType" suffix. + param_type = type(self).__name__.partition("ParamType")[0] + param_type = param_type.partition("ParameterType")[0] + + # Custom subclasses might not remember to set a name. + if hasattr(self, "name"): + name = self.name + else: + name = param_type + + return {"param_type": param_type, "name": name} + + def __call__( + self, + value: t.Any, + param: Parameter | None = None, + ctx: Context | None = None, + ) -> t.Any: + if value is not None: + return self.convert(value, param, ctx) + + def get_metavar(self, param: Parameter, ctx: Context) -> str | None: + """Returns the metavar default for this param if it provides one.""" + + def get_missing_message(self, param: Parameter, ctx: Context | None) -> str | None: + """Optionally might return extra information about a missing + parameter. + + .. versionadded:: 2.0 + """ + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + """Convert the value to the correct type. This is not called if + the value is ``None`` (the missing value). + + This must accept string values from the command line, as well as + values that are already the correct type. It may also convert + other compatible types. + + The ``param`` and ``ctx`` arguments may be ``None`` in certain + situations, such as when converting prompt input. + + If the value cannot be converted, call :meth:`fail` with a + descriptive message. + + :param value: The value to convert. + :param param: The parameter that is using this type to convert + its value. May be ``None``. + :param ctx: The current context that arrived at this value. May + be ``None``. + """ + return value + + def split_envvar_value(self, rv: str) -> cabc.Sequence[str]: + """Given a value from an environment variable this splits it up + into small chunks depending on the defined envvar list splitter. + + If the splitter is set to `None`, which means that whitespace splits, + then leading and trailing whitespace is ignored. Otherwise, leading + and trailing splitters usually lead to empty items being included. + """ + return (rv or "").split(self.envvar_list_splitter) + + def fail( + self, + message: str, + param: Parameter | None = None, + ctx: Context | None = None, + ) -> t.NoReturn: + """Helper method to fail with an invalid value message.""" + raise BadParameter(message, ctx=ctx, param=param) + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Return a list of + :class:`~click.shell_completion.CompletionItem` objects for the + incomplete value. Most types do not provide completions, but + some do, and this allows custom types to provide custom + completions as well. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + return [] + + +class CompositeParamType(ParamType): + is_composite = True + + @property + def arity(self) -> int: # type: ignore + raise NotImplementedError() + + +class FuncParamType(ParamType): + def __init__(self, func: t.Callable[[t.Any], t.Any]) -> None: + self.name: str = func.__name__ + self.func = func + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["func"] = self.func + return info_dict + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + try: + return self.func(value) + except ValueError: + try: + value = str(value) + except UnicodeError: + value = value.decode("utf-8", "replace") + + self.fail(value, param, ctx) + + +class UnprocessedParamType(ParamType): + name = "text" + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + return value + + def __repr__(self) -> str: + return "UNPROCESSED" + + +class StringParamType(ParamType): + name = "text" + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + if isinstance(value, bytes): + enc = _get_argv_encoding() + try: + value = value.decode(enc) + except UnicodeError: + fs_enc = sys.getfilesystemencoding() + if fs_enc != enc: + try: + value = value.decode(fs_enc) + except UnicodeError: + value = value.decode("utf-8", "replace") + else: + value = value.decode("utf-8", "replace") + return value + return str(value) + + def __repr__(self) -> str: + return "STRING" + + +class Choice(ParamType, t.Generic[ParamTypeValue]): + """The choice type allows a value to be checked against a fixed set + of supported values. + + You may pass any iterable value which will be converted to a tuple + and thus will only be iterated once. + + The resulting value will always be one of the originally passed choices. + See :meth:`normalize_choice` for more info on the mapping of strings + to choices. See :ref:`choice-opts` for an example. + + :param case_sensitive: Set to false to make choices case + insensitive. Defaults to true. + + .. versionchanged:: 8.2.0 + Non-``str`` ``choices`` are now supported. It can additionally be any + iterable. Before you were not recommended to pass anything but a list or + tuple. + + .. versionadded:: 8.2.0 + Choice normalization can be overridden via :meth:`normalize_choice`. + """ + + name = "choice" + + def __init__( + self, choices: cabc.Iterable[ParamTypeValue], case_sensitive: bool = True + ) -> None: + self.choices: cabc.Sequence[ParamTypeValue] = tuple(choices) + self.case_sensitive = case_sensitive + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["choices"] = self.choices + info_dict["case_sensitive"] = self.case_sensitive + return info_dict + + def _normalized_mapping( + self, ctx: Context | None = None + ) -> cabc.Mapping[ParamTypeValue, str]: + """ + Returns mapping where keys are the original choices and the values are + the normalized values that are accepted via the command line. + + This is a simple wrapper around :meth:`normalize_choice`, use that + instead which is supported. + """ + return { + choice: self.normalize_choice( + choice=choice, + ctx=ctx, + ) + for choice in self.choices + } + + def normalize_choice(self, choice: ParamTypeValue, ctx: Context | None) -> str: + """ + Normalize a choice value, used to map a passed string to a choice. + Each choice must have a unique normalized value. + + By default uses :meth:`Context.token_normalize_func` and if not case + sensitive, convert it to a casefolded value. + + .. versionadded:: 8.2.0 + """ + normed_value = choice.name if isinstance(choice, enum.Enum) else str(choice) + + if ctx is not None and ctx.token_normalize_func is not None: + normed_value = ctx.token_normalize_func(normed_value) + + if not self.case_sensitive: + normed_value = normed_value.casefold() + + return normed_value + + def get_metavar(self, param: Parameter, ctx: Context) -> str | None: + if param.param_type_name == "option" and not param.show_choices: # type: ignore + choice_metavars = [ + convert_type(type(choice)).name.upper() for choice in self.choices + ] + choices_str = "|".join([*dict.fromkeys(choice_metavars)]) + else: + choices_str = "|".join( + [str(i) for i in self._normalized_mapping(ctx=ctx).values()] + ) + + # Use curly braces to indicate a required argument. + if param.required and param.param_type_name == "argument": + return f"{{{choices_str}}}" + + # Use square braces to indicate an option or optional argument. + return f"[{choices_str}]" + + def get_missing_message(self, param: Parameter, ctx: Context | None) -> str: + """ + Message shown when no choice is passed. + + .. versionchanged:: 8.2.0 Added ``ctx`` argument. + """ + return _("Choose from:\n\t{choices}").format( + choices=",\n\t".join(self._normalized_mapping(ctx=ctx).values()) + ) + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> ParamTypeValue: + """ + For a given value from the parser, normalize it and find its + matching normalized value in the list of choices. Then return the + matched "original" choice. + """ + normed_value = self.normalize_choice(choice=value, ctx=ctx) + normalized_mapping = self._normalized_mapping(ctx=ctx) + + try: + return next( + original + for original, normalized in normalized_mapping.items() + if normalized == normed_value + ) + except StopIteration: + self.fail( + self.get_invalid_choice_message(value=value, ctx=ctx), + param=param, + ctx=ctx, + ) + + def get_invalid_choice_message(self, value: t.Any, ctx: Context | None) -> str: + """Get the error message when the given choice is invalid. + + :param value: The invalid value. + + .. versionadded:: 8.2 + """ + choices_str = ", ".join(map(repr, self._normalized_mapping(ctx=ctx).values())) + return ngettext( + "{value!r} is not {choice}.", + "{value!r} is not one of {choices}.", + len(self.choices), + ).format(value=value, choice=choices_str, choices=choices_str) + + def __repr__(self) -> str: + return f"Choice({list(self.choices)})" + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Complete choices that start with the incomplete value. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + str_choices = map(str, self.choices) + + if self.case_sensitive: + matched = (c for c in str_choices if c.startswith(incomplete)) + else: + incomplete = incomplete.lower() + matched = (c for c in str_choices if c.lower().startswith(incomplete)) + + return [CompletionItem(c) for c in matched] + + +class DateTime(ParamType): + """The DateTime type converts date strings into `datetime` objects. + + The format strings which are checked are configurable, but default to some + common (non-timezone aware) ISO 8601 formats. + + When specifying *DateTime* formats, you should only pass a list or a tuple. + Other iterables, like generators, may lead to surprising results. + + The format strings are processed using ``datetime.strptime``, and this + consequently defines the format strings which are allowed. + + Parsing is tried using each format, in order, and the first format which + parses successfully is used. + + :param formats: A list or tuple of date format strings, in the order in + which they should be tried. Defaults to + ``'%Y-%m-%d'``, ``'%Y-%m-%dT%H:%M:%S'``, + ``'%Y-%m-%d %H:%M:%S'``. + """ + + name = "datetime" + + def __init__(self, formats: cabc.Sequence[str] | None = None): + self.formats: cabc.Sequence[str] = formats or [ + "%Y-%m-%d", + "%Y-%m-%dT%H:%M:%S", + "%Y-%m-%d %H:%M:%S", + ] + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["formats"] = self.formats + return info_dict + + def get_metavar(self, param: Parameter, ctx: Context) -> str | None: + return f"[{'|'.join(self.formats)}]" + + def _try_to_convert_date(self, value: t.Any, format: str) -> datetime | None: + try: + return datetime.strptime(value, format) + except ValueError: + return None + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + if isinstance(value, datetime): + return value + + for format in self.formats: + converted = self._try_to_convert_date(value, format) + + if converted is not None: + return converted + + formats_str = ", ".join(map(repr, self.formats)) + self.fail( + ngettext( + "{value!r} does not match the format {format}.", + "{value!r} does not match the formats {formats}.", + len(self.formats), + ).format(value=value, format=formats_str, formats=formats_str), + param, + ctx, + ) + + def __repr__(self) -> str: + return "DateTime" + + +class _NumberParamTypeBase(ParamType): + _number_class: t.ClassVar[type[t.Any]] + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + try: + return self._number_class(value) + except ValueError: + self.fail( + _("{value!r} is not a valid {number_type}.").format( + value=value, number_type=self.name + ), + param, + ctx, + ) + + +class _NumberRangeBase(_NumberParamTypeBase): + def __init__( + self, + min: float | None = None, + max: float | None = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + self.min = min + self.max = max + self.min_open = min_open + self.max_open = max_open + self.clamp = clamp + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + min=self.min, + max=self.max, + min_open=self.min_open, + max_open=self.max_open, + clamp=self.clamp, + ) + return info_dict + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + import operator + + rv = super().convert(value, param, ctx) + lt_min: bool = self.min is not None and ( + operator.le if self.min_open else operator.lt + )(rv, self.min) + gt_max: bool = self.max is not None and ( + operator.ge if self.max_open else operator.gt + )(rv, self.max) + + if self.clamp: + if lt_min: + return self._clamp(self.min, 1, self.min_open) # type: ignore + + if gt_max: + return self._clamp(self.max, -1, self.max_open) # type: ignore + + if lt_min or gt_max: + self.fail( + _("{value} is not in the range {range}.").format( + value=rv, range=self._describe_range() + ), + param, + ctx, + ) + + return rv + + def _clamp(self, bound: float, dir: t.Literal[1, -1], open: bool) -> float: + """Find the valid value to clamp to bound in the given + direction. + + :param bound: The boundary value. + :param dir: 1 or -1 indicating the direction to move. + :param open: If true, the range does not include the bound. + """ + raise NotImplementedError + + def _describe_range(self) -> str: + """Describe the range for use in help text.""" + if self.min is None: + op = "<" if self.max_open else "<=" + return f"x{op}{self.max}" + + if self.max is None: + op = ">" if self.min_open else ">=" + return f"x{op}{self.min}" + + lop = "<" if self.min_open else "<=" + rop = "<" if self.max_open else "<=" + return f"{self.min}{lop}x{rop}{self.max}" + + def __repr__(self) -> str: + clamp = " clamped" if self.clamp else "" + return f"<{type(self).__name__} {self._describe_range()}{clamp}>" + + +class IntParamType(_NumberParamTypeBase): + name = "integer" + _number_class = int + + def __repr__(self) -> str: + return "INT" + + +class IntRange(_NumberRangeBase, IntParamType): + """Restrict an :data:`click.INT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "integer range" + + def _clamp( # type: ignore + self, bound: int, dir: t.Literal[1, -1], open: bool + ) -> int: + if not open: + return bound + + return bound + dir + + +class FloatParamType(_NumberParamTypeBase): + name = "float" + _number_class = float + + def __repr__(self) -> str: + return "FLOAT" + + +class FloatRange(_NumberRangeBase, FloatParamType): + """Restrict a :data:`click.FLOAT` value to a range of accepted + values. See :ref:`ranges`. + + If ``min`` or ``max`` are not passed, any value is accepted in that + direction. If ``min_open`` or ``max_open`` are enabled, the + corresponding boundary is not included in the range. + + If ``clamp`` is enabled, a value outside the range is clamped to the + boundary instead of failing. This is not supported if either + boundary is marked ``open``. + + .. versionchanged:: 8.0 + Added the ``min_open`` and ``max_open`` parameters. + """ + + name = "float range" + + def __init__( + self, + min: float | None = None, + max: float | None = None, + min_open: bool = False, + max_open: bool = False, + clamp: bool = False, + ) -> None: + super().__init__( + min=min, max=max, min_open=min_open, max_open=max_open, clamp=clamp + ) + + if (min_open or max_open) and clamp: + raise TypeError("Clamping is not supported for open bounds.") + + def _clamp(self, bound: float, dir: t.Literal[1, -1], open: bool) -> float: + if not open: + return bound + + # Could use math.nextafter here, but clamping an + # open float range doesn't seem to be particularly useful. It's + # left up to the user to write a callback to do it if needed. + raise RuntimeError("Clamping is not supported for open bounds.") + + +class BoolParamType(ParamType): + name = "boolean" + + bool_states: dict[str, bool] = { + "1": True, + "0": False, + "yes": True, + "no": False, + "true": True, + "false": False, + "on": True, + "off": False, + "t": True, + "f": False, + "y": True, + "n": False, + # Absence of value is considered False. + "": False, + } + """A mapping of string values to boolean states. + + Mapping is inspired by :py:attr:`configparser.ConfigParser.BOOLEAN_STATES` + and extends it. + + .. caution:: + String values are lower-cased, as the ``str_to_bool`` comparison function + below is case-insensitive. + + .. warning:: + The mapping is not exhaustive, and does not cover all possible boolean strings + representations. It will remains as it is to avoid endless bikeshedding. + + Future work my be considered to make this mapping user-configurable from public + API. + """ + + @staticmethod + def str_to_bool(value: str | bool) -> bool | None: + """Convert a string to a boolean value. + + If the value is already a boolean, it is returned as-is. If the value is a + string, it is stripped of whitespaces and lower-cased, then checked against + the known boolean states pre-defined in the `BoolParamType.bool_states` mapping + above. + + Returns `None` if the value does not match any known boolean state. + """ + if isinstance(value, bool): + return value + return BoolParamType.bool_states.get(value.strip().lower()) + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> bool: + normalized = self.str_to_bool(value) + if normalized is None: + self.fail( + _( + "{value!r} is not a valid boolean. Recognized values: {states}" + ).format(value=value, states=", ".join(sorted(self.bool_states))), + param, + ctx, + ) + return normalized + + def __repr__(self) -> str: + return "BOOL" + + +class UUIDParameterType(ParamType): + name = "uuid" + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + import uuid + + if isinstance(value, uuid.UUID): + return value + + value = value.strip() + + try: + return uuid.UUID(value) + except ValueError: + self.fail( + _("{value!r} is not a valid UUID.").format(value=value), param, ctx + ) + + def __repr__(self) -> str: + return "UUID" + + +class File(ParamType): + """Declares a parameter to be a file for reading or writing. The file + is automatically closed once the context tears down (after the command + finished working). + + Files can be opened for reading or writing. The special value ``-`` + indicates stdin or stdout depending on the mode. + + By default, the file is opened for reading text data, but it can also be + opened in binary mode or for writing. The encoding parameter can be used + to force a specific encoding. + + The `lazy` flag controls if the file should be opened immediately or upon + first IO. The default is to be non-lazy for standard input and output + streams as well as files opened for reading, `lazy` otherwise. When opening a + file lazily for reading, it is still opened temporarily for validation, but + will not be held open until first IO. lazy is mainly useful when opening + for writing to avoid creating the file until it is needed. + + Files can also be opened atomically in which case all writes go into a + separate file in the same folder and upon completion the file will + be moved over to the original location. This is useful if a file + regularly read by other users is modified. + + See :ref:`file-args` for more information. + + .. versionchanged:: 2.0 + Added the ``atomic`` parameter. + """ + + name = "filename" + envvar_list_splitter: t.ClassVar[str] = os.path.pathsep + + def __init__( + self, + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool | None = None, + atomic: bool = False, + ) -> None: + self.mode = mode + self.encoding = encoding + self.errors = errors + self.lazy = lazy + self.atomic = atomic + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update(mode=self.mode, encoding=self.encoding) + return info_dict + + def resolve_lazy_flag(self, value: str | os.PathLike[str]) -> bool: + if self.lazy is not None: + return self.lazy + if os.fspath(value) == "-": + return False + elif "w" in self.mode: + return True + return False + + def convert( + self, + value: str | os.PathLike[str] | t.IO[t.Any], + param: Parameter | None, + ctx: Context | None, + ) -> t.IO[t.Any]: + if _is_file_like(value): + return value + + value = t.cast("str | os.PathLike[str]", value) + + try: + lazy = self.resolve_lazy_flag(value) + + if lazy: + lf = LazyFile( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + + if ctx is not None: + ctx.call_on_close(lf.close_intelligently) + + return t.cast("t.IO[t.Any]", lf) + + f, should_close = open_stream( + value, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + + # If a context is provided, we automatically close the file + # at the end of the context execution (or flush out). If a + # context does not exist, it's the caller's responsibility to + # properly close the file. This for instance happens when the + # type is used with prompts. + if ctx is not None: + if should_close: + ctx.call_on_close(safecall(f.close)) + else: + ctx.call_on_close(safecall(f.flush)) + + return f + except OSError as e: + self.fail(f"'{format_filename(value)}': {e.strerror}", param, ctx) + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Return a special completion marker that tells the completion + system to use the shell to provide file path completions. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + return [CompletionItem(incomplete, type="file")] + + +def _is_file_like(value: t.Any) -> te.TypeGuard[t.IO[t.Any]]: + return hasattr(value, "read") or hasattr(value, "write") + + +class Path(ParamType): + """The ``Path`` type is similar to the :class:`File` type, but + returns the filename instead of an open file. Various checks can be + enabled to validate the type of file and permissions. + + :param exists: The file or directory needs to exist for the value to + be valid. If this is not set to ``True``, and the file does not + exist, then all further checks are silently skipped. + :param file_okay: Allow a file as a value. + :param dir_okay: Allow a directory as a value. + :param readable: if true, a readable check is performed. + :param writable: if true, a writable check is performed. + :param executable: if true, an executable check is performed. + :param resolve_path: Make the value absolute and resolve any + symlinks. A ``~`` is not expanded, as this is supposed to be + done by the shell only. + :param allow_dash: Allow a single dash as a value, which indicates + a standard stream (but does not open it). Use + :func:`~click.open_file` to handle opening this value. + :param path_type: Convert the incoming path value to this type. If + ``None``, keep Python's default, which is ``str``. Useful to + convert to :class:`pathlib.Path`. + + .. versionchanged:: 8.1 + Added the ``executable`` parameter. + + .. versionchanged:: 8.0 + Allow passing ``path_type=pathlib.Path``. + + .. versionchanged:: 6.0 + Added the ``allow_dash`` parameter. + """ + + envvar_list_splitter: t.ClassVar[str] = os.path.pathsep + + def __init__( + self, + exists: bool = False, + file_okay: bool = True, + dir_okay: bool = True, + writable: bool = False, + readable: bool = True, + resolve_path: bool = False, + allow_dash: bool = False, + path_type: type[t.Any] | None = None, + executable: bool = False, + ): + self.exists = exists + self.file_okay = file_okay + self.dir_okay = dir_okay + self.readable = readable + self.writable = writable + self.executable = executable + self.resolve_path = resolve_path + self.allow_dash = allow_dash + self.type = path_type + + if self.file_okay and not self.dir_okay: + self.name: str = _("file") + elif self.dir_okay and not self.file_okay: + self.name = _("directory") + else: + self.name = _("path") + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict.update( + exists=self.exists, + file_okay=self.file_okay, + dir_okay=self.dir_okay, + writable=self.writable, + readable=self.readable, + allow_dash=self.allow_dash, + ) + return info_dict + + def coerce_path_result( + self, value: str | os.PathLike[str] + ) -> str | bytes | os.PathLike[str]: + if self.type is not None and not isinstance(value, self.type): + if self.type is str: + return os.fsdecode(value) + elif self.type is bytes: + return os.fsencode(value) + else: + return t.cast("os.PathLike[str]", self.type(value)) + + return value + + def convert( + self, + value: str | os.PathLike[str], + param: Parameter | None, + ctx: Context | None, + ) -> str | bytes | os.PathLike[str]: + rv = value + + is_dash = self.file_okay and self.allow_dash and rv in (b"-", "-") + + if not is_dash: + if self.resolve_path: + rv = os.path.realpath(rv) + + try: + st = os.stat(rv) + except OSError: + if not self.exists: + return self.coerce_path_result(rv) + self.fail( + _("{name} {filename!r} does not exist.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if not self.file_okay and stat.S_ISREG(st.st_mode): + self.fail( + _("{name} {filename!r} is a file.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + if not self.dir_okay and stat.S_ISDIR(st.st_mode): + self.fail( + _("{name} {filename!r} is a directory.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.readable and not os.access(rv, os.R_OK): + self.fail( + _("{name} {filename!r} is not readable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.writable and not os.access(rv, os.W_OK): + self.fail( + _("{name} {filename!r} is not writable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + if self.executable and not os.access(value, os.X_OK): + self.fail( + _("{name} {filename!r} is not executable.").format( + name=self.name.title(), filename=format_filename(value) + ), + param, + ctx, + ) + + return self.coerce_path_result(rv) + + def shell_complete( + self, ctx: Context, param: Parameter, incomplete: str + ) -> list[CompletionItem]: + """Return a special completion marker that tells the completion + system to use the shell to provide path completions for only + directories or any paths. + + :param ctx: Invocation context for this command. + :param param: The parameter that is requesting completion. + :param incomplete: Value being completed. May be empty. + + .. versionadded:: 8.0 + """ + from click.shell_completion import CompletionItem + + type = "dir" if self.dir_okay and not self.file_okay else "file" + return [CompletionItem(incomplete, type=type)] + + +class Tuple(CompositeParamType): + """The default behavior of Click is to apply a type on a value directly. + This works well in most cases, except for when `nargs` is set to a fixed + count and different types should be used for different items. In this + case the :class:`Tuple` type can be used. This type can only be used + if `nargs` is set to a fixed number. + + For more information see :ref:`tuple-type`. + + This can be selected by using a Python tuple literal as a type. + + :param types: a list of types that should be used for the tuple items. + """ + + def __init__(self, types: cabc.Sequence[type[t.Any] | ParamType]) -> None: + self.types: cabc.Sequence[ParamType] = [convert_type(ty) for ty in types] + + def to_info_dict(self) -> dict[str, t.Any]: + info_dict = super().to_info_dict() + info_dict["types"] = [t.to_info_dict() for t in self.types] + return info_dict + + @property + def name(self) -> str: # type: ignore + return f"<{' '.join(ty.name for ty in self.types)}>" + + @property + def arity(self) -> int: # type: ignore + return len(self.types) + + def convert( + self, value: t.Any, param: Parameter | None, ctx: Context | None + ) -> t.Any: + len_type = len(self.types) + len_value = len(value) + + if len_value != len_type: + self.fail( + ngettext( + "{len_type} values are required, but {len_value} was given.", + "{len_type} values are required, but {len_value} were given.", + len_value, + ).format(len_type=len_type, len_value=len_value), + param=param, + ctx=ctx, + ) + + return tuple( + ty(x, param, ctx) for ty, x in zip(self.types, value, strict=False) + ) + + +def convert_type(ty: t.Any | None, default: t.Any | None = None) -> ParamType: + """Find the most appropriate :class:`ParamType` for the given Python + type. If the type isn't provided, it can be inferred from a default + value. + """ + guessed_type = False + + if ty is None and default is not None: + if isinstance(default, (tuple, list)): + # If the default is empty, ty will remain None and will + # return STRING. + if default: + item = default[0] + + # A tuple of tuples needs to detect the inner types. + # Can't call convert recursively because that would + # incorrectly unwind the tuple to a single type. + if isinstance(item, (tuple, list)): + ty = tuple(map(type, item)) + else: + ty = type(item) + else: + ty = type(default) + + guessed_type = True + + if isinstance(ty, tuple): + return Tuple(ty) + + if isinstance(ty, ParamType): + return ty + + if ty is str or ty is None: + return STRING + + if ty is int: + return INT + + if ty is float: + return FLOAT + + if ty is bool: + return BOOL + + if guessed_type: + return STRING + + if __debug__: + try: + if issubclass(ty, ParamType): + raise AssertionError( + f"Attempted to use an uninstantiated parameter type ({ty})." + ) + except TypeError: + # ty is an instance (correct), so issubclass fails. + pass + + return FuncParamType(ty) + + +#: A dummy parameter type that just does nothing. From a user's +#: perspective this appears to just be the same as `STRING` but +#: internally no string conversion takes place if the input was bytes. +#: This is usually useful when working with file paths as they can +#: appear in bytes and unicode. +#: +#: For path related uses the :class:`Path` type is a better choice but +#: there are situations where an unprocessed type is useful which is why +#: it is is provided. +#: +#: .. versionadded:: 4.0 +UNPROCESSED = UnprocessedParamType() + +#: A unicode string parameter type which is the implicit default. This +#: can also be selected by using ``str`` as type. +STRING = StringParamType() + +#: An integer parameter. This can also be selected by using ``int`` as +#: type. +INT = IntParamType() + +#: A floating point value parameter. This can also be selected by using +#: ``float`` as type. +FLOAT = FloatParamType() + +#: A boolean parameter. This is the default for boolean flags. This can +#: also be selected by using ``bool`` as a type. +BOOL = BoolParamType() + +#: A UUID parameter. +UUID = UUIDParameterType() + + +class OptionHelpExtra(t.TypedDict, total=False): + envvars: tuple[str, ...] + default: str + range: str + required: str diff --git a/netdeploy/lib/python3.11/site-packages/click/utils.py b/netdeploy/lib/python3.11/site-packages/click/utils.py new file mode 100644 index 0000000..beae26f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/click/utils.py @@ -0,0 +1,627 @@ +from __future__ import annotations + +import collections.abc as cabc +import os +import re +import sys +import typing as t +from functools import update_wrapper +from types import ModuleType +from types import TracebackType + +from ._compat import _default_text_stderr +from ._compat import _default_text_stdout +from ._compat import _find_binary_writer +from ._compat import auto_wrap_for_ansi +from ._compat import binary_streams +from ._compat import open_stream +from ._compat import should_strip_ansi +from ._compat import strip_ansi +from ._compat import text_streams +from ._compat import WIN +from .globals import resolve_color_default + +if t.TYPE_CHECKING: + import typing_extensions as te + + P = te.ParamSpec("P") + +R = t.TypeVar("R") + + +def _posixify(name: str) -> str: + return "-".join(name.split()).lower() + + +def safecall(func: t.Callable[P, R]) -> t.Callable[P, R | None]: + """Wraps a function so that it swallows exceptions.""" + + def wrapper(*args: P.args, **kwargs: P.kwargs) -> R | None: + try: + return func(*args, **kwargs) + except Exception: + pass + return None + + return update_wrapper(wrapper, func) + + +def make_str(value: t.Any) -> str: + """Converts a value into a valid string.""" + if isinstance(value, bytes): + try: + return value.decode(sys.getfilesystemencoding()) + except UnicodeError: + return value.decode("utf-8", "replace") + return str(value) + + +def make_default_short_help(help: str, max_length: int = 45) -> str: + """Returns a condensed version of help string.""" + # Consider only the first paragraph. + paragraph_end = help.find("\n\n") + + if paragraph_end != -1: + help = help[:paragraph_end] + + # Collapse newlines, tabs, and spaces. + words = help.split() + + if not words: + return "" + + # The first paragraph started with a "no rewrap" marker, ignore it. + if words[0] == "\b": + words = words[1:] + + total_length = 0 + last_index = len(words) - 1 + + for i, word in enumerate(words): + total_length += len(word) + (i > 0) + + if total_length > max_length: # too long, truncate + break + + if word[-1] == ".": # sentence end, truncate without "..." + return " ".join(words[: i + 1]) + + if total_length == max_length and i != last_index: + break # not at sentence end, truncate with "..." + else: + return " ".join(words) # no truncation needed + + # Account for the length of the suffix. + total_length += len("...") + + # remove words until the length is short enough + while i > 0: + total_length -= len(words[i]) + (i > 0) + + if total_length <= max_length: + break + + i -= 1 + + return " ".join(words[:i]) + "..." + + +class LazyFile: + """A lazy file works like a regular file but it does not fully open + the file but it does perform some basic checks early to see if the + filename parameter does make sense. This is useful for safely opening + files for writing. + """ + + def __init__( + self, + filename: str | os.PathLike[str], + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + atomic: bool = False, + ): + self.name: str = os.fspath(filename) + self.mode = mode + self.encoding = encoding + self.errors = errors + self.atomic = atomic + self._f: t.IO[t.Any] | None + self.should_close: bool + + if self.name == "-": + self._f, self.should_close = open_stream(filename, mode, encoding, errors) + else: + if "r" in mode: + # Open and close the file in case we're opening it for + # reading so that we can catch at least some errors in + # some cases early. + open(filename, mode).close() + self._f = None + self.should_close = True + + def __getattr__(self, name: str) -> t.Any: + return getattr(self.open(), name) + + def __repr__(self) -> str: + if self._f is not None: + return repr(self._f) + return f"" + + def open(self) -> t.IO[t.Any]: + """Opens the file if it's not yet open. This call might fail with + a :exc:`FileError`. Not handling this error will produce an error + that Click shows. + """ + if self._f is not None: + return self._f + try: + rv, self.should_close = open_stream( + self.name, self.mode, self.encoding, self.errors, atomic=self.atomic + ) + except OSError as e: + from .exceptions import FileError + + raise FileError(self.name, hint=e.strerror) from e + self._f = rv + return rv + + def close(self) -> None: + """Closes the underlying file, no matter what.""" + if self._f is not None: + self._f.close() + + def close_intelligently(self) -> None: + """This function only closes the file if it was opened by the lazy + file wrapper. For instance this will never close stdin. + """ + if self.should_close: + self.close() + + def __enter__(self) -> LazyFile: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.close_intelligently() + + def __iter__(self) -> cabc.Iterator[t.AnyStr]: + self.open() + return iter(self._f) # type: ignore + + +class KeepOpenFile: + def __init__(self, file: t.IO[t.Any]) -> None: + self._file: t.IO[t.Any] = file + + def __getattr__(self, name: str) -> t.Any: + return getattr(self._file, name) + + def __enter__(self) -> KeepOpenFile: + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + pass + + def __repr__(self) -> str: + return repr(self._file) + + def __iter__(self) -> cabc.Iterator[t.AnyStr]: + return iter(self._file) + + +def echo( + message: t.Any | None = None, + file: t.IO[t.Any] | None = None, + nl: bool = True, + err: bool = False, + color: bool | None = None, +) -> None: + """Print a message and newline to stdout or a file. This should be + used instead of :func:`print` because it provides better support + for different data, files, and environments. + + Compared to :func:`print`, this does the following: + + - Ensures that the output encoding is not misconfigured on Linux. + - Supports Unicode in the Windows console. + - Supports writing to binary outputs, and supports writing bytes + to text outputs. + - Supports colors and styles on Windows. + - Removes ANSI color and style codes if the output does not look + like an interactive terminal. + - Always flushes the output. + + :param message: The string or bytes to output. Other objects are + converted to strings. + :param file: The file to write to. Defaults to ``stdout``. + :param err: Write to ``stderr`` instead of ``stdout``. + :param nl: Print a newline after the message. Enabled by default. + :param color: Force showing or hiding colors and other styles. By + default Click will remove color if the output does not look like + an interactive terminal. + + .. versionchanged:: 6.0 + Support Unicode output on the Windows console. Click does not + modify ``sys.stdout``, so ``sys.stdout.write()`` and ``print()`` + will still not support Unicode. + + .. versionchanged:: 4.0 + Added the ``color`` parameter. + + .. versionadded:: 3.0 + Added the ``err`` parameter. + + .. versionchanged:: 2.0 + Support colors on Windows if colorama is installed. + """ + if file is None: + if err: + file = _default_text_stderr() + else: + file = _default_text_stdout() + + # There are no standard streams attached to write to. For example, + # pythonw on Windows. + if file is None: + return + + # Convert non bytes/text into the native string type. + if message is not None and not isinstance(message, (str, bytes, bytearray)): + out: str | bytes | bytearray | None = str(message) + else: + out = message + + if nl: + out = out or "" + if isinstance(out, str): + out += "\n" + else: + out += b"\n" + + if not out: + file.flush() + return + + # If there is a message and the value looks like bytes, we manually + # need to find the binary stream and write the message in there. + # This is done separately so that most stream types will work as you + # would expect. Eg: you can write to StringIO for other cases. + if isinstance(out, (bytes, bytearray)): + binary_file = _find_binary_writer(file) + + if binary_file is not None: + file.flush() + binary_file.write(out) + binary_file.flush() + return + + # ANSI style code support. For no message or bytes, nothing happens. + # When outputting to a file instead of a terminal, strip codes. + else: + color = resolve_color_default(color) + + if should_strip_ansi(file, color): + out = strip_ansi(out) + elif WIN: + if auto_wrap_for_ansi is not None: + file = auto_wrap_for_ansi(file, color) # type: ignore + elif not color: + out = strip_ansi(out) + + file.write(out) # type: ignore + file.flush() + + +def get_binary_stream(name: t.Literal["stdin", "stdout", "stderr"]) -> t.BinaryIO: + """Returns a system stream for byte processing. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + """ + opener = binary_streams.get(name) + if opener is None: + raise TypeError(f"Unknown standard stream '{name}'") + return opener() + + +def get_text_stream( + name: t.Literal["stdin", "stdout", "stderr"], + encoding: str | None = None, + errors: str | None = "strict", +) -> t.TextIO: + """Returns a system stream for text processing. This usually returns + a wrapped stream around a binary stream returned from + :func:`get_binary_stream` but it also can take shortcuts for already + correctly configured streams. + + :param name: the name of the stream to open. Valid names are ``'stdin'``, + ``'stdout'`` and ``'stderr'`` + :param encoding: overrides the detected default encoding. + :param errors: overrides the default error mode. + """ + opener = text_streams.get(name) + if opener is None: + raise TypeError(f"Unknown standard stream '{name}'") + return opener(encoding, errors) + + +def open_file( + filename: str | os.PathLike[str], + mode: str = "r", + encoding: str | None = None, + errors: str | None = "strict", + lazy: bool = False, + atomic: bool = False, +) -> t.IO[t.Any]: + """Open a file, with extra behavior to handle ``'-'`` to indicate + a standard stream, lazy open on write, and atomic write. Similar to + the behavior of the :class:`~click.File` param type. + + If ``'-'`` is given to open ``stdout`` or ``stdin``, the stream is + wrapped so that using it in a context manager will not close it. + This makes it possible to use the function without accidentally + closing a standard stream: + + .. code-block:: python + + with open_file(filename) as f: + ... + + :param filename: The name or Path of the file to open, or ``'-'`` for + ``stdin``/``stdout``. + :param mode: The mode in which to open the file. + :param encoding: The encoding to decode or encode a file opened in + text mode. + :param errors: The error handling mode. + :param lazy: Wait to open the file until it is accessed. For read + mode, the file is temporarily opened to raise access errors + early, then closed until it is read again. + :param atomic: Write to a temporary file and replace the given file + on close. + + .. versionadded:: 3.0 + """ + if lazy: + return t.cast( + "t.IO[t.Any]", LazyFile(filename, mode, encoding, errors, atomic=atomic) + ) + + f, should_close = open_stream(filename, mode, encoding, errors, atomic=atomic) + + if not should_close: + f = t.cast("t.IO[t.Any]", KeepOpenFile(f)) + + return f + + +def format_filename( + filename: str | bytes | os.PathLike[str] | os.PathLike[bytes], + shorten: bool = False, +) -> str: + """Format a filename as a string for display. Ensures the filename can be + displayed by replacing any invalid bytes or surrogate escapes in the name + with the replacement character ``�``. + + Invalid bytes or surrogate escapes will raise an error when written to a + stream with ``errors="strict"``. This will typically happen with ``stdout`` + when the locale is something like ``en_GB.UTF-8``. + + Many scenarios *are* safe to write surrogates though, due to PEP 538 and + PEP 540, including: + + - Writing to ``stderr``, which uses ``errors="backslashreplace"``. + - The system has ``LANG=C.UTF-8``, ``C``, or ``POSIX``. Python opens + stdout and stderr with ``errors="surrogateescape"``. + - None of ``LANG/LC_*`` are set. Python assumes ``LANG=C.UTF-8``. + - Python is started in UTF-8 mode with ``PYTHONUTF8=1`` or ``-X utf8``. + Python opens stdout and stderr with ``errors="surrogateescape"``. + + :param filename: formats a filename for UI display. This will also convert + the filename into unicode without failing. + :param shorten: this optionally shortens the filename to strip of the + path that leads up to it. + """ + if shorten: + filename = os.path.basename(filename) + else: + filename = os.fspath(filename) + + if isinstance(filename, bytes): + filename = filename.decode(sys.getfilesystemencoding(), "replace") + else: + filename = filename.encode("utf-8", "surrogateescape").decode( + "utf-8", "replace" + ) + + return filename + + +def get_app_dir(app_name: str, roaming: bool = True, force_posix: bool = False) -> str: + r"""Returns the config folder for the application. The default behavior + is to return whatever is most appropriate for the operating system. + + To give you an idea, for an app called ``"Foo Bar"``, something like + the following folders could be returned: + + Mac OS X: + ``~/Library/Application Support/Foo Bar`` + Mac OS X (POSIX): + ``~/.foo-bar`` + Unix: + ``~/.config/foo-bar`` + Unix (POSIX): + ``~/.foo-bar`` + Windows (roaming): + ``C:\Users\\AppData\Roaming\Foo Bar`` + Windows (not roaming): + ``C:\Users\\AppData\Local\Foo Bar`` + + .. versionadded:: 2.0 + + :param app_name: the application name. This should be properly capitalized + and can contain whitespace. + :param roaming: controls if the folder should be roaming or not on Windows. + Has no effect otherwise. + :param force_posix: if this is set to `True` then on any POSIX system the + folder will be stored in the home folder with a leading + dot instead of the XDG config home or darwin's + application support folder. + """ + if WIN: + key = "APPDATA" if roaming else "LOCALAPPDATA" + folder = os.environ.get(key) + if folder is None: + folder = os.path.expanduser("~") + return os.path.join(folder, app_name) + if force_posix: + return os.path.join(os.path.expanduser(f"~/.{_posixify(app_name)}")) + if sys.platform == "darwin": + return os.path.join( + os.path.expanduser("~/Library/Application Support"), app_name + ) + return os.path.join( + os.environ.get("XDG_CONFIG_HOME", os.path.expanduser("~/.config")), + _posixify(app_name), + ) + + +class PacifyFlushWrapper: + """This wrapper is used to catch and suppress BrokenPipeErrors resulting + from ``.flush()`` being called on broken pipe during the shutdown/final-GC + of the Python interpreter. Notably ``.flush()`` is always called on + ``sys.stdout`` and ``sys.stderr``. So as to have minimal impact on any + other cleanup code, and the case where the underlying file is not a broken + pipe, all calls and attributes are proxied. + """ + + def __init__(self, wrapped: t.IO[t.Any]) -> None: + self.wrapped = wrapped + + def flush(self) -> None: + try: + self.wrapped.flush() + except OSError as e: + import errno + + if e.errno != errno.EPIPE: + raise + + def __getattr__(self, attr: str) -> t.Any: + return getattr(self.wrapped, attr) + + +def _detect_program_name( + path: str | None = None, _main: ModuleType | None = None +) -> str: + """Determine the command used to run the program, for use in help + text. If a file or entry point was executed, the file name is + returned. If ``python -m`` was used to execute a module or package, + ``python -m name`` is returned. + + This doesn't try to be too precise, the goal is to give a concise + name for help text. Files are only shown as their name without the + path. ``python`` is only shown for modules, and the full path to + ``sys.executable`` is not shown. + + :param path: The Python file being executed. Python puts this in + ``sys.argv[0]``, which is used by default. + :param _main: The ``__main__`` module. This should only be passed + during internal testing. + + .. versionadded:: 8.0 + Based on command args detection in the Werkzeug reloader. + + :meta private: + """ + if _main is None: + _main = sys.modules["__main__"] + + if not path: + path = sys.argv[0] + + # The value of __package__ indicates how Python was called. It may + # not exist if a setuptools script is installed as an egg. It may be + # set incorrectly for entry points created with pip on Windows. + # It is set to "" inside a Shiv or PEX zipapp. + if getattr(_main, "__package__", None) in {None, ""} or ( + os.name == "nt" + and _main.__package__ == "" + and not os.path.exists(path) + and os.path.exists(f"{path}.exe") + ): + # Executed a file, like "python app.py". + return os.path.basename(path) + + # Executed a module, like "python -m example". + # Rewritten by Python from "-m script" to "/path/to/script.py". + # Need to look at main module to determine how it was executed. + py_module = t.cast(str, _main.__package__) + name = os.path.splitext(os.path.basename(path))[0] + + # A submodule like "example.cli". + if name != "__main__": + py_module = f"{py_module}.{name}" + + return f"python -m {py_module.lstrip('.')}" + + +def _expand_args( + args: cabc.Iterable[str], + *, + user: bool = True, + env: bool = True, + glob_recursive: bool = True, +) -> list[str]: + """Simulate Unix shell expansion with Python functions. + + See :func:`glob.glob`, :func:`os.path.expanduser`, and + :func:`os.path.expandvars`. + + This is intended for use on Windows, where the shell does not do any + expansion. It may not exactly match what a Unix shell would do. + + :param args: List of command line arguments to expand. + :param user: Expand user home directory. + :param env: Expand environment variables. + :param glob_recursive: ``**`` matches directories recursively. + + .. versionchanged:: 8.1 + Invalid glob patterns are treated as empty expansions rather + than raising an error. + + .. versionadded:: 8.0 + + :meta private: + """ + from glob import glob + + out = [] + + for arg in args: + if user: + arg = os.path.expanduser(arg) + + if env: + arg = os.path.expandvars(arg) + + try: + matches = glob(arg, recursive=glob_recursive) + except re.error: + matches = [] + + if not matches: + out.append(arg) + else: + out.extend(matches) + + return out diff --git a/netdeploy/lib/python3.11/site-packages/distutils-precedence.pth b/netdeploy/lib/python3.11/site-packages/distutils-precedence.pth new file mode 100644 index 0000000..7f009fe --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/distutils-precedence.pth @@ -0,0 +1 @@ +import os; var = 'SETUPTOOLS_USE_DISTUTILS'; enabled = os.environ.get(var, 'local') == 'local'; enabled and __import__('_distutils_hack').add_shim(); diff --git a/netdeploy/lib/python3.11/site-packages/dns/__init__.py b/netdeploy/lib/python3.11/site-packages/dns/__init__.py new file mode 100644 index 0000000..d30fd74 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/__init__.py @@ -0,0 +1,72 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009, 2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""dnspython DNS toolkit""" + +__all__ = [ + "asyncbackend", + "asyncquery", + "asyncresolver", + "btree", + "btreezone", + "dnssec", + "dnssecalgs", + "dnssectypes", + "e164", + "edns", + "entropy", + "exception", + "flags", + "immutable", + "inet", + "ipv4", + "ipv6", + "message", + "name", + "namedict", + "node", + "opcode", + "query", + "quic", + "rcode", + "rdata", + "rdataclass", + "rdataset", + "rdatatype", + "renderer", + "resolver", + "reversename", + "rrset", + "serial", + "set", + "tokenizer", + "transaction", + "tsig", + "tsigkeyring", + "ttl", + "rdtypes", + "update", + "version", + "versioned", + "wire", + "xfr", + "zone", + "zonetypes", + "zonefile", +] + +from dns.version import version as __version__ # noqa diff --git a/netdeploy/lib/python3.11/site-packages/dns/_asyncbackend.py b/netdeploy/lib/python3.11/site-packages/dns/_asyncbackend.py new file mode 100644 index 0000000..23455db --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/_asyncbackend.py @@ -0,0 +1,100 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# This is a nullcontext for both sync and async. 3.7 has a nullcontext, +# but it is only for sync use. + + +class NullContext: + def __init__(self, enter_result=None): + self.enter_result = enter_result + + def __enter__(self): + return self.enter_result + + def __exit__(self, exc_type, exc_value, traceback): + pass + + async def __aenter__(self): + return self.enter_result + + async def __aexit__(self, exc_type, exc_value, traceback): + pass + + +# These are declared here so backends can import them without creating +# circular dependencies with dns.asyncbackend. + + +class Socket: # pragma: no cover + def __init__(self, family: int, type: int): + self.family = family + self.type = type + + async def close(self): + pass + + async def getpeername(self): + raise NotImplementedError + + async def getsockname(self): + raise NotImplementedError + + async def getpeercert(self, timeout): + raise NotImplementedError + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_value, traceback): + await self.close() + + +class DatagramSocket(Socket): # pragma: no cover + async def sendto(self, what, destination, timeout): + raise NotImplementedError + + async def recvfrom(self, size, timeout): + raise NotImplementedError + + +class StreamSocket(Socket): # pragma: no cover + async def sendall(self, what, timeout): + raise NotImplementedError + + async def recv(self, size, timeout): + raise NotImplementedError + + +class NullTransport: + async def connect_tcp(self, host, port, timeout, local_address): + raise NotImplementedError + + +class Backend: # pragma: no cover + def name(self) -> str: + return "unknown" + + async def make_socket( + self, + af, + socktype, + proto=0, + source=None, + destination=None, + timeout=None, + ssl_context=None, + server_hostname=None, + ): + raise NotImplementedError + + def datagram_connection_required(self): + return False + + async def sleep(self, interval): + raise NotImplementedError + + def get_transport_class(self): + raise NotImplementedError + + async def wait_for(self, awaitable, timeout): + raise NotImplementedError diff --git a/netdeploy/lib/python3.11/site-packages/dns/_asyncio_backend.py b/netdeploy/lib/python3.11/site-packages/dns/_asyncio_backend.py new file mode 100644 index 0000000..303908c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/_asyncio_backend.py @@ -0,0 +1,276 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +"""asyncio library query support""" + +import asyncio +import socket +import sys + +import dns._asyncbackend +import dns._features +import dns.exception +import dns.inet + +_is_win32 = sys.platform == "win32" + + +def _get_running_loop(): + try: + return asyncio.get_running_loop() + except AttributeError: # pragma: no cover + return asyncio.get_event_loop() + + +class _DatagramProtocol: + def __init__(self): + self.transport = None + self.recvfrom = None + + def connection_made(self, transport): + self.transport = transport + + def datagram_received(self, data, addr): + if self.recvfrom and not self.recvfrom.done(): + self.recvfrom.set_result((data, addr)) + + def error_received(self, exc): # pragma: no cover + if self.recvfrom and not self.recvfrom.done(): + self.recvfrom.set_exception(exc) + + def connection_lost(self, exc): + if self.recvfrom and not self.recvfrom.done(): + if exc is None: + # EOF we triggered. Is there a better way to do this? + try: + raise EOFError("EOF") + except EOFError as e: + self.recvfrom.set_exception(e) + else: + self.recvfrom.set_exception(exc) + + def close(self): + if self.transport is not None: + self.transport.close() + + +async def _maybe_wait_for(awaitable, timeout): + if timeout is not None: + try: + return await asyncio.wait_for(awaitable, timeout) + except asyncio.TimeoutError: + raise dns.exception.Timeout(timeout=timeout) + else: + return await awaitable + + +class DatagramSocket(dns._asyncbackend.DatagramSocket): + def __init__(self, family, transport, protocol): + super().__init__(family, socket.SOCK_DGRAM) + self.transport = transport + self.protocol = protocol + + async def sendto(self, what, destination, timeout): # pragma: no cover + # no timeout for asyncio sendto + self.transport.sendto(what, destination) + return len(what) + + async def recvfrom(self, size, timeout): + # ignore size as there's no way I know to tell protocol about it + done = _get_running_loop().create_future() + try: + assert self.protocol.recvfrom is None + self.protocol.recvfrom = done + await _maybe_wait_for(done, timeout) + return done.result() + finally: + self.protocol.recvfrom = None + + async def close(self): + self.protocol.close() + + async def getpeername(self): + return self.transport.get_extra_info("peername") + + async def getsockname(self): + return self.transport.get_extra_info("sockname") + + async def getpeercert(self, timeout): + raise NotImplementedError + + +class StreamSocket(dns._asyncbackend.StreamSocket): + def __init__(self, af, reader, writer): + super().__init__(af, socket.SOCK_STREAM) + self.reader = reader + self.writer = writer + + async def sendall(self, what, timeout): + self.writer.write(what) + return await _maybe_wait_for(self.writer.drain(), timeout) + + async def recv(self, size, timeout): + return await _maybe_wait_for(self.reader.read(size), timeout) + + async def close(self): + self.writer.close() + + async def getpeername(self): + return self.writer.get_extra_info("peername") + + async def getsockname(self): + return self.writer.get_extra_info("sockname") + + async def getpeercert(self, timeout): + return self.writer.get_extra_info("peercert") + + +if dns._features.have("doh"): + import anyio + import httpcore + import httpcore._backends.anyio + import httpx + + _CoreAsyncNetworkBackend = httpcore.AsyncNetworkBackend + _CoreAnyIOStream = httpcore._backends.anyio.AnyIOStream # pyright: ignore + + from dns.query import _compute_times, _expiration_for_this_attempt, _remaining + + class _NetworkBackend(_CoreAsyncNetworkBackend): + def __init__(self, resolver, local_port, bootstrap_address, family): + super().__init__() + self._local_port = local_port + self._resolver = resolver + self._bootstrap_address = bootstrap_address + self._family = family + if local_port != 0: + raise NotImplementedError( + "the asyncio transport for HTTPX cannot set the local port" + ) + + async def connect_tcp( + self, host, port, timeout=None, local_address=None, socket_options=None + ): # pylint: disable=signature-differs + addresses = [] + _, expiration = _compute_times(timeout) + if dns.inet.is_address(host): + addresses.append(host) + elif self._bootstrap_address is not None: + addresses.append(self._bootstrap_address) + else: + timeout = _remaining(expiration) + family = self._family + if local_address: + family = dns.inet.af_for_address(local_address) + answers = await self._resolver.resolve_name( + host, family=family, lifetime=timeout + ) + addresses = answers.addresses() + for address in addresses: + try: + attempt_expiration = _expiration_for_this_attempt(2.0, expiration) + timeout = _remaining(attempt_expiration) + with anyio.fail_after(timeout): + stream = await anyio.connect_tcp( + remote_host=address, + remote_port=port, + local_host=local_address, + ) + return _CoreAnyIOStream(stream) + except Exception: + pass + raise httpcore.ConnectError + + async def connect_unix_socket( + self, path, timeout=None, socket_options=None + ): # pylint: disable=signature-differs + raise NotImplementedError + + async def sleep(self, seconds): # pylint: disable=signature-differs + await anyio.sleep(seconds) + + class _HTTPTransport(httpx.AsyncHTTPTransport): + def __init__( + self, + *args, + local_port=0, + bootstrap_address=None, + resolver=None, + family=socket.AF_UNSPEC, + **kwargs, + ): + if resolver is None and bootstrap_address is None: + # pylint: disable=import-outside-toplevel,redefined-outer-name + import dns.asyncresolver + + resolver = dns.asyncresolver.Resolver() + super().__init__(*args, **kwargs) + self._pool._network_backend = _NetworkBackend( + resolver, local_port, bootstrap_address, family + ) + +else: + _HTTPTransport = dns._asyncbackend.NullTransport # type: ignore + + +class Backend(dns._asyncbackend.Backend): + def name(self): + return "asyncio" + + async def make_socket( + self, + af, + socktype, + proto=0, + source=None, + destination=None, + timeout=None, + ssl_context=None, + server_hostname=None, + ): + loop = _get_running_loop() + if socktype == socket.SOCK_DGRAM: + if _is_win32 and source is None: + # Win32 wants explicit binding before recvfrom(). This is the + # proper fix for [#637]. + source = (dns.inet.any_for_af(af), 0) + transport, protocol = await loop.create_datagram_endpoint( + _DatagramProtocol, # pyright: ignore + source, + family=af, + proto=proto, + remote_addr=destination, + ) + return DatagramSocket(af, transport, protocol) + elif socktype == socket.SOCK_STREAM: + if destination is None: + # This shouldn't happen, but we check to make code analysis software + # happier. + raise ValueError("destination required for stream sockets") + (r, w) = await _maybe_wait_for( + asyncio.open_connection( + destination[0], + destination[1], + ssl=ssl_context, + family=af, + proto=proto, + local_addr=source, + server_hostname=server_hostname, + ), + timeout, + ) + return StreamSocket(af, r, w) + raise NotImplementedError( + "unsupported socket " + f"type {socktype}" + ) # pragma: no cover + + async def sleep(self, interval): + await asyncio.sleep(interval) + + def datagram_connection_required(self): + return False + + def get_transport_class(self): + return _HTTPTransport + + async def wait_for(self, awaitable, timeout): + return await _maybe_wait_for(awaitable, timeout) diff --git a/netdeploy/lib/python3.11/site-packages/dns/_ddr.py b/netdeploy/lib/python3.11/site-packages/dns/_ddr.py new file mode 100644 index 0000000..bf5c11e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/_ddr.py @@ -0,0 +1,154 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license +# +# Support for Discovery of Designated Resolvers + +import socket +import time +from urllib.parse import urlparse + +import dns.asyncbackend +import dns.inet +import dns.name +import dns.nameserver +import dns.query +import dns.rdtypes.svcbbase + +# The special name of the local resolver when using DDR +_local_resolver_name = dns.name.from_text("_dns.resolver.arpa") + + +# +# Processing is split up into I/O independent and I/O dependent parts to +# make supporting sync and async versions easy. +# + + +class _SVCBInfo: + def __init__(self, bootstrap_address, port, hostname, nameservers): + self.bootstrap_address = bootstrap_address + self.port = port + self.hostname = hostname + self.nameservers = nameservers + + def ddr_check_certificate(self, cert): + """Verify that the _SVCBInfo's address is in the cert's subjectAltName (SAN)""" + for name, value in cert["subjectAltName"]: + if name == "IP Address" and value == self.bootstrap_address: + return True + return False + + def make_tls_context(self): + ssl = dns.query.ssl + ctx = ssl.create_default_context() + ctx.minimum_version = ssl.TLSVersion.TLSv1_2 + return ctx + + def ddr_tls_check_sync(self, lifetime): + ctx = self.make_tls_context() + expiration = time.time() + lifetime + with socket.create_connection( + (self.bootstrap_address, self.port), lifetime + ) as s: + with ctx.wrap_socket(s, server_hostname=self.hostname) as ts: + ts.settimeout(dns.query._remaining(expiration)) + ts.do_handshake() + cert = ts.getpeercert() + return self.ddr_check_certificate(cert) + + async def ddr_tls_check_async(self, lifetime, backend=None): + if backend is None: + backend = dns.asyncbackend.get_default_backend() + ctx = self.make_tls_context() + expiration = time.time() + lifetime + async with await backend.make_socket( + dns.inet.af_for_address(self.bootstrap_address), + socket.SOCK_STREAM, + 0, + None, + (self.bootstrap_address, self.port), + lifetime, + ctx, + self.hostname, + ) as ts: + cert = await ts.getpeercert(dns.query._remaining(expiration)) + return self.ddr_check_certificate(cert) + + +def _extract_nameservers_from_svcb(answer): + bootstrap_address = answer.nameserver + if not dns.inet.is_address(bootstrap_address): + return [] + infos = [] + for rr in answer.rrset.processing_order(): + nameservers = [] + param = rr.params.get(dns.rdtypes.svcbbase.ParamKey.ALPN) + if param is None: + continue + alpns = set(param.ids) + host = rr.target.to_text(omit_final_dot=True) + port = None + param = rr.params.get(dns.rdtypes.svcbbase.ParamKey.PORT) + if param is not None: + port = param.port + # For now we ignore address hints and address resolution and always use the + # bootstrap address + if b"h2" in alpns: + param = rr.params.get(dns.rdtypes.svcbbase.ParamKey.DOHPATH) + if param is None or not param.value.endswith(b"{?dns}"): + continue + path = param.value[:-6].decode() + if not path.startswith("/"): + path = "/" + path + if port is None: + port = 443 + url = f"https://{host}:{port}{path}" + # check the URL + try: + urlparse(url) + nameservers.append(dns.nameserver.DoHNameserver(url, bootstrap_address)) + except Exception: + # continue processing other ALPN types + pass + if b"dot" in alpns: + if port is None: + port = 853 + nameservers.append( + dns.nameserver.DoTNameserver(bootstrap_address, port, host) + ) + if b"doq" in alpns: + if port is None: + port = 853 + nameservers.append( + dns.nameserver.DoQNameserver(bootstrap_address, port, True, host) + ) + if len(nameservers) > 0: + infos.append(_SVCBInfo(bootstrap_address, port, host, nameservers)) + return infos + + +def _get_nameservers_sync(answer, lifetime): + """Return a list of TLS-validated resolver nameservers extracted from an SVCB + answer.""" + nameservers = [] + infos = _extract_nameservers_from_svcb(answer) + for info in infos: + try: + if info.ddr_tls_check_sync(lifetime): + nameservers.extend(info.nameservers) + except Exception: + pass + return nameservers + + +async def _get_nameservers_async(answer, lifetime): + """Return a list of TLS-validated resolver nameservers extracted from an SVCB + answer.""" + nameservers = [] + infos = _extract_nameservers_from_svcb(answer) + for info in infos: + try: + if await info.ddr_tls_check_async(lifetime): + nameservers.extend(info.nameservers) + except Exception: + pass + return nameservers diff --git a/netdeploy/lib/python3.11/site-packages/dns/_features.py b/netdeploy/lib/python3.11/site-packages/dns/_features.py new file mode 100644 index 0000000..65a9a2a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/_features.py @@ -0,0 +1,95 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import importlib.metadata +import itertools +import string +from typing import Dict, List, Tuple + + +def _tuple_from_text(version: str) -> Tuple: + text_parts = version.split(".") + int_parts = [] + for text_part in text_parts: + digit_prefix = "".join( + itertools.takewhile(lambda x: x in string.digits, text_part) + ) + try: + int_parts.append(int(digit_prefix)) + except Exception: + break + return tuple(int_parts) + + +def _version_check( + requirement: str, +) -> bool: + """Is the requirement fulfilled? + + The requirement must be of the form + + package>=version + """ + package, minimum = requirement.split(">=") + try: + version = importlib.metadata.version(package) + # This shouldn't happen, but it apparently can. + if version is None: + return False + except Exception: + return False + t_version = _tuple_from_text(version) + t_minimum = _tuple_from_text(minimum) + if t_version < t_minimum: + return False + return True + + +_cache: Dict[str, bool] = {} + + +def have(feature: str) -> bool: + """Is *feature* available? + + This tests if all optional packages needed for the + feature are available and recent enough. + + Returns ``True`` if the feature is available, + and ``False`` if it is not or if metadata is + missing. + """ + value = _cache.get(feature) + if value is not None: + return value + requirements = _requirements.get(feature) + if requirements is None: + # we make a cache entry here for consistency not performance + _cache[feature] = False + return False + ok = True + for requirement in requirements: + if not _version_check(requirement): + ok = False + break + _cache[feature] = ok + return ok + + +def force(feature: str, enabled: bool) -> None: + """Force the status of *feature* to be *enabled*. + + This method is provided as a workaround for any cases + where importlib.metadata is ineffective, or for testing. + """ + _cache[feature] = enabled + + +_requirements: Dict[str, List[str]] = { + ### BEGIN generated requirements + "dnssec": ["cryptography>=45"], + "doh": ["httpcore>=1.0.0", "httpx>=0.28.0", "h2>=4.2.0"], + "doq": ["aioquic>=1.2.0"], + "idna": ["idna>=3.10"], + "trio": ["trio>=0.30"], + "wmi": ["wmi>=1.5.1"], + ### END generated requirements +} diff --git a/netdeploy/lib/python3.11/site-packages/dns/_immutable_ctx.py b/netdeploy/lib/python3.11/site-packages/dns/_immutable_ctx.py new file mode 100644 index 0000000..b3d72de --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/_immutable_ctx.py @@ -0,0 +1,76 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# This implementation of the immutable decorator requires python >= +# 3.7, and is significantly more storage efficient when making classes +# with slots immutable. It's also faster. + +import contextvars +import inspect + +_in__init__ = contextvars.ContextVar("_immutable_in__init__", default=False) + + +class _Immutable: + """Immutable mixin class""" + + # We set slots to the empty list to say "we don't have any attributes". + # We do this so that if we're mixed in with a class with __slots__, we + # don't cause a __dict__ to be added which would waste space. + + __slots__ = () + + def __setattr__(self, name, value): + if _in__init__.get() is not self: + raise TypeError("object doesn't support attribute assignment") + else: + super().__setattr__(name, value) + + def __delattr__(self, name): + if _in__init__.get() is not self: + raise TypeError("object doesn't support attribute assignment") + else: + super().__delattr__(name) + + +def _immutable_init(f): + def nf(*args, **kwargs): + previous = _in__init__.set(args[0]) + try: + # call the actual __init__ + f(*args, **kwargs) + finally: + _in__init__.reset(previous) + + nf.__signature__ = inspect.signature(f) # pyright: ignore + return nf + + +def immutable(cls): + if _Immutable in cls.__mro__: + # Some ancestor already has the mixin, so just make sure we keep + # following the __init__ protocol. + cls.__init__ = _immutable_init(cls.__init__) + if hasattr(cls, "__setstate__"): + cls.__setstate__ = _immutable_init(cls.__setstate__) + ncls = cls + else: + # Mixin the Immutable class and follow the __init__ protocol. + class ncls(_Immutable, cls): + # We have to do the __slots__ declaration here too! + __slots__ = () + + @_immutable_init + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + if hasattr(cls, "__setstate__"): + + @_immutable_init + def __setstate__(self, *args, **kwargs): + super().__setstate__(*args, **kwargs) + + # make ncls have the same name and module as cls + ncls.__name__ = cls.__name__ + ncls.__qualname__ = cls.__qualname__ + ncls.__module__ = cls.__module__ + return ncls diff --git a/netdeploy/lib/python3.11/site-packages/dns/_no_ssl.py b/netdeploy/lib/python3.11/site-packages/dns/_no_ssl.py new file mode 100644 index 0000000..edb452d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/_no_ssl.py @@ -0,0 +1,61 @@ +import enum +from typing import Any + +CERT_NONE = 0 + + +class TLSVersion(enum.IntEnum): + TLSv1_2 = 12 + + +class WantReadException(Exception): + pass + + +class WantWriteException(Exception): + pass + + +class SSLWantReadError(Exception): + pass + + +class SSLWantWriteError(Exception): + pass + + +class SSLContext: + def __init__(self) -> None: + self.minimum_version: Any = TLSVersion.TLSv1_2 + self.check_hostname: bool = False + self.verify_mode: int = CERT_NONE + + def wrap_socket(self, *args, **kwargs) -> "SSLSocket": # type: ignore + raise Exception("no ssl support") # pylint: disable=broad-exception-raised + + def set_alpn_protocols(self, *args, **kwargs): # type: ignore + raise Exception("no ssl support") # pylint: disable=broad-exception-raised + + +class SSLSocket: + def pending(self) -> bool: + raise Exception("no ssl support") # pylint: disable=broad-exception-raised + + def do_handshake(self) -> None: + raise Exception("no ssl support") # pylint: disable=broad-exception-raised + + def settimeout(self, value: Any) -> None: + pass + + def getpeercert(self) -> Any: + raise Exception("no ssl support") # pylint: disable=broad-exception-raised + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + return False + + +def create_default_context(*args, **kwargs) -> SSLContext: # type: ignore + raise Exception("no ssl support") # pylint: disable=broad-exception-raised diff --git a/netdeploy/lib/python3.11/site-packages/dns/_tls_util.py b/netdeploy/lib/python3.11/site-packages/dns/_tls_util.py new file mode 100644 index 0000000..10ddf72 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/_tls_util.py @@ -0,0 +1,19 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import os +from typing import Tuple + + +def convert_verify_to_cafile_and_capath( + verify: bool | str, +) -> Tuple[str | None, str | None]: + cafile: str | None = None + capath: str | None = None + if isinstance(verify, str): + if os.path.isfile(verify): + cafile = verify + elif os.path.isdir(verify): + capath = verify + else: + raise ValueError("invalid verify string") + return cafile, capath diff --git a/netdeploy/lib/python3.11/site-packages/dns/_trio_backend.py b/netdeploy/lib/python3.11/site-packages/dns/_trio_backend.py new file mode 100644 index 0000000..bde7e8b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/_trio_backend.py @@ -0,0 +1,255 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +"""trio async I/O library query support""" + +import socket + +import trio +import trio.socket # type: ignore + +import dns._asyncbackend +import dns._features +import dns.exception +import dns.inet + +if not dns._features.have("trio"): + raise ImportError("trio not found or too old") + + +def _maybe_timeout(timeout): + if timeout is not None: + return trio.move_on_after(timeout) + else: + return dns._asyncbackend.NullContext() + + +# for brevity +_lltuple = dns.inet.low_level_address_tuple + +# pylint: disable=redefined-outer-name + + +class DatagramSocket(dns._asyncbackend.DatagramSocket): + def __init__(self, sock): + super().__init__(sock.family, socket.SOCK_DGRAM) + self.socket = sock + + async def sendto(self, what, destination, timeout): + with _maybe_timeout(timeout): + if destination is None: + return await self.socket.send(what) + else: + return await self.socket.sendto(what, destination) + raise dns.exception.Timeout( + timeout=timeout + ) # pragma: no cover lgtm[py/unreachable-statement] + + async def recvfrom(self, size, timeout): + with _maybe_timeout(timeout): + return await self.socket.recvfrom(size) + raise dns.exception.Timeout(timeout=timeout) # lgtm[py/unreachable-statement] + + async def close(self): + self.socket.close() + + async def getpeername(self): + return self.socket.getpeername() + + async def getsockname(self): + return self.socket.getsockname() + + async def getpeercert(self, timeout): + raise NotImplementedError + + +class StreamSocket(dns._asyncbackend.StreamSocket): + def __init__(self, family, stream, tls=False): + super().__init__(family, socket.SOCK_STREAM) + self.stream = stream + self.tls = tls + + async def sendall(self, what, timeout): + with _maybe_timeout(timeout): + return await self.stream.send_all(what) + raise dns.exception.Timeout(timeout=timeout) # lgtm[py/unreachable-statement] + + async def recv(self, size, timeout): + with _maybe_timeout(timeout): + return await self.stream.receive_some(size) + raise dns.exception.Timeout(timeout=timeout) # lgtm[py/unreachable-statement] + + async def close(self): + await self.stream.aclose() + + async def getpeername(self): + if self.tls: + return self.stream.transport_stream.socket.getpeername() + else: + return self.stream.socket.getpeername() + + async def getsockname(self): + if self.tls: + return self.stream.transport_stream.socket.getsockname() + else: + return self.stream.socket.getsockname() + + async def getpeercert(self, timeout): + if self.tls: + with _maybe_timeout(timeout): + await self.stream.do_handshake() + return self.stream.getpeercert() + else: + raise NotImplementedError + + +if dns._features.have("doh"): + import httpcore + import httpcore._backends.trio + import httpx + + _CoreAsyncNetworkBackend = httpcore.AsyncNetworkBackend + _CoreTrioStream = httpcore._backends.trio.TrioStream + + from dns.query import _compute_times, _expiration_for_this_attempt, _remaining + + class _NetworkBackend(_CoreAsyncNetworkBackend): + def __init__(self, resolver, local_port, bootstrap_address, family): + super().__init__() + self._local_port = local_port + self._resolver = resolver + self._bootstrap_address = bootstrap_address + self._family = family + + async def connect_tcp( + self, host, port, timeout=None, local_address=None, socket_options=None + ): # pylint: disable=signature-differs + addresses = [] + _, expiration = _compute_times(timeout) + if dns.inet.is_address(host): + addresses.append(host) + elif self._bootstrap_address is not None: + addresses.append(self._bootstrap_address) + else: + timeout = _remaining(expiration) + family = self._family + if local_address: + family = dns.inet.af_for_address(local_address) + answers = await self._resolver.resolve_name( + host, family=family, lifetime=timeout + ) + addresses = answers.addresses() + for address in addresses: + try: + af = dns.inet.af_for_address(address) + if local_address is not None or self._local_port != 0: + source = (local_address, self._local_port) + else: + source = None + destination = (address, port) + attempt_expiration = _expiration_for_this_attempt(2.0, expiration) + timeout = _remaining(attempt_expiration) + sock = await Backend().make_socket( + af, socket.SOCK_STREAM, 0, source, destination, timeout + ) + assert isinstance(sock, StreamSocket) + return _CoreTrioStream(sock.stream) + except Exception: + continue + raise httpcore.ConnectError + + async def connect_unix_socket( + self, path, timeout=None, socket_options=None + ): # pylint: disable=signature-differs + raise NotImplementedError + + async def sleep(self, seconds): # pylint: disable=signature-differs + await trio.sleep(seconds) + + class _HTTPTransport(httpx.AsyncHTTPTransport): + def __init__( + self, + *args, + local_port=0, + bootstrap_address=None, + resolver=None, + family=socket.AF_UNSPEC, + **kwargs, + ): + if resolver is None and bootstrap_address is None: + # pylint: disable=import-outside-toplevel,redefined-outer-name + import dns.asyncresolver + + resolver = dns.asyncresolver.Resolver() + super().__init__(*args, **kwargs) + self._pool._network_backend = _NetworkBackend( + resolver, local_port, bootstrap_address, family + ) + +else: + _HTTPTransport = dns._asyncbackend.NullTransport # type: ignore + + +class Backend(dns._asyncbackend.Backend): + def name(self): + return "trio" + + async def make_socket( + self, + af, + socktype, + proto=0, + source=None, + destination=None, + timeout=None, + ssl_context=None, + server_hostname=None, + ): + s = trio.socket.socket(af, socktype, proto) + stream = None + try: + if source: + await s.bind(_lltuple(source, af)) + if socktype == socket.SOCK_STREAM or destination is not None: + connected = False + with _maybe_timeout(timeout): + assert destination is not None + await s.connect(_lltuple(destination, af)) + connected = True + if not connected: + raise dns.exception.Timeout( + timeout=timeout + ) # lgtm[py/unreachable-statement] + except Exception: # pragma: no cover + s.close() + raise + if socktype == socket.SOCK_DGRAM: + return DatagramSocket(s) + elif socktype == socket.SOCK_STREAM: + stream = trio.SocketStream(s) + tls = False + if ssl_context: + tls = True + try: + stream = trio.SSLStream( + stream, ssl_context, server_hostname=server_hostname + ) + except Exception: # pragma: no cover + await stream.aclose() + raise + return StreamSocket(af, stream, tls) + raise NotImplementedError( + "unsupported socket " + f"type {socktype}" + ) # pragma: no cover + + async def sleep(self, interval): + await trio.sleep(interval) + + def get_transport_class(self): + return _HTTPTransport + + async def wait_for(self, awaitable, timeout): + with _maybe_timeout(timeout): + return await awaitable + raise dns.exception.Timeout( + timeout=timeout + ) # pragma: no cover lgtm[py/unreachable-statement] diff --git a/netdeploy/lib/python3.11/site-packages/dns/asyncbackend.py b/netdeploy/lib/python3.11/site-packages/dns/asyncbackend.py new file mode 100644 index 0000000..0ec58b0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/asyncbackend.py @@ -0,0 +1,101 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +from typing import Dict + +import dns.exception + +# pylint: disable=unused-import +from dns._asyncbackend import ( # noqa: F401 lgtm[py/unused-import] + Backend, + DatagramSocket, + Socket, + StreamSocket, +) + +# pylint: enable=unused-import + +_default_backend = None + +_backends: Dict[str, Backend] = {} + +# Allow sniffio import to be disabled for testing purposes +_no_sniffio = False + + +class AsyncLibraryNotFoundError(dns.exception.DNSException): + pass + + +def get_backend(name: str) -> Backend: + """Get the specified asynchronous backend. + + *name*, a ``str``, the name of the backend. Currently the "trio" + and "asyncio" backends are available. + + Raises NotImplementedError if an unknown backend name is specified. + """ + # pylint: disable=import-outside-toplevel,redefined-outer-name + backend = _backends.get(name) + if backend: + return backend + if name == "trio": + import dns._trio_backend + + backend = dns._trio_backend.Backend() + elif name == "asyncio": + import dns._asyncio_backend + + backend = dns._asyncio_backend.Backend() + else: + raise NotImplementedError(f"unimplemented async backend {name}") + _backends[name] = backend + return backend + + +def sniff() -> str: + """Attempt to determine the in-use asynchronous I/O library by using + the ``sniffio`` module if it is available. + + Returns the name of the library, or raises AsyncLibraryNotFoundError + if the library cannot be determined. + """ + # pylint: disable=import-outside-toplevel + try: + if _no_sniffio: + raise ImportError + import sniffio + + try: + return sniffio.current_async_library() + except sniffio.AsyncLibraryNotFoundError: + raise AsyncLibraryNotFoundError("sniffio cannot determine async library") + except ImportError: + import asyncio + + try: + asyncio.get_running_loop() + return "asyncio" + except RuntimeError: + raise AsyncLibraryNotFoundError("no async library detected") + + +def get_default_backend() -> Backend: + """Get the default backend, initializing it if necessary.""" + if _default_backend: + return _default_backend + + return set_default_backend(sniff()) + + +def set_default_backend(name: str) -> Backend: + """Set the default backend. + + It's not normally necessary to call this method, as + ``get_default_backend()`` will initialize the backend + appropriately in many cases. If ``sniffio`` is not installed, or + in testing situations, this function allows the backend to be set + explicitly. + """ + global _default_backend + _default_backend = get_backend(name) + return _default_backend diff --git a/netdeploy/lib/python3.11/site-packages/dns/asyncquery.py b/netdeploy/lib/python3.11/site-packages/dns/asyncquery.py new file mode 100644 index 0000000..bb77045 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/asyncquery.py @@ -0,0 +1,953 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Talk to a DNS server.""" + +import base64 +import contextlib +import random +import socket +import struct +import time +import urllib.parse +from typing import Any, Dict, Optional, Tuple, cast + +import dns.asyncbackend +import dns.exception +import dns.inet +import dns.message +import dns.name +import dns.quic +import dns.rdatatype +import dns.transaction +import dns.tsig +import dns.xfr +from dns._asyncbackend import NullContext +from dns.query import ( + BadResponse, + HTTPVersion, + NoDOH, + NoDOQ, + UDPMode, + _check_status, + _compute_times, + _matches_destination, + _remaining, + have_doh, + make_ssl_context, +) + +try: + import ssl +except ImportError: + import dns._no_ssl as ssl # type: ignore + +if have_doh: + import httpx + +# for brevity +_lltuple = dns.inet.low_level_address_tuple + + +def _source_tuple(af, address, port): + # Make a high level source tuple, or return None if address and port + # are both None + if address or port: + if address is None: + if af == socket.AF_INET: + address = "0.0.0.0" + elif af == socket.AF_INET6: + address = "::" + else: + raise NotImplementedError(f"unknown address family {af}") + return (address, port) + else: + return None + + +def _timeout(expiration, now=None): + if expiration is not None: + if not now: + now = time.time() + return max(expiration - now, 0) + else: + return None + + +async def send_udp( + sock: dns.asyncbackend.DatagramSocket, + what: dns.message.Message | bytes, + destination: Any, + expiration: float | None = None, +) -> Tuple[int, float]: + """Send a DNS message to the specified UDP socket. + + *sock*, a ``dns.asyncbackend.DatagramSocket``. + + *what*, a ``bytes`` or ``dns.message.Message``, the message to send. + + *destination*, a destination tuple appropriate for the address family + of the socket, specifying where to send the query. + + *expiration*, a ``float`` or ``None``, the absolute time at which + a timeout exception should be raised. If ``None``, no timeout will + occur. The expiration value is meaningless for the asyncio backend, as + asyncio's transport sendto() never blocks. + + Returns an ``(int, float)`` tuple of bytes sent and the sent time. + """ + + if isinstance(what, dns.message.Message): + what = what.to_wire() + sent_time = time.time() + n = await sock.sendto(what, destination, _timeout(expiration, sent_time)) + return (n, sent_time) + + +async def receive_udp( + sock: dns.asyncbackend.DatagramSocket, + destination: Any | None = None, + expiration: float | None = None, + ignore_unexpected: bool = False, + one_rr_per_rrset: bool = False, + keyring: Dict[dns.name.Name, dns.tsig.Key] | None = None, + request_mac: bytes | None = b"", + ignore_trailing: bool = False, + raise_on_truncation: bool = False, + ignore_errors: bool = False, + query: dns.message.Message | None = None, +) -> Any: + """Read a DNS message from a UDP socket. + + *sock*, a ``dns.asyncbackend.DatagramSocket``. + + See :py:func:`dns.query.receive_udp()` for the documentation of the other + parameters, and exceptions. + + Returns a ``(dns.message.Message, float, tuple)`` tuple of the received message, the + received time, and the address where the message arrived from. + """ + + wire = b"" + while True: + (wire, from_address) = await sock.recvfrom(65535, _timeout(expiration)) + if not _matches_destination( + sock.family, from_address, destination, ignore_unexpected + ): + continue + received_time = time.time() + try: + r = dns.message.from_wire( + wire, + keyring=keyring, + request_mac=request_mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + raise_on_truncation=raise_on_truncation, + ) + except dns.message.Truncated as e: + # See the comment in query.py for details. + if ( + ignore_errors + and query is not None + and not query.is_response(e.message()) + ): + continue + else: + raise + except Exception: + if ignore_errors: + continue + else: + raise + if ignore_errors and query is not None and not query.is_response(r): + continue + return (r, received_time, from_address) + + +async def udp( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 53, + source: str | None = None, + source_port: int = 0, + ignore_unexpected: bool = False, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + raise_on_truncation: bool = False, + sock: dns.asyncbackend.DatagramSocket | None = None, + backend: dns.asyncbackend.Backend | None = None, + ignore_errors: bool = False, +) -> dns.message.Message: + """Return the response obtained after sending a query via UDP. + + *sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``, + the socket to use for the query. If ``None``, the default, a + socket is created. Note that if a socket is provided, the + *source*, *source_port*, and *backend* are ignored. + + *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``, + the default, then dnspython will use the default backend. + + See :py:func:`dns.query.udp()` for the documentation of the other + parameters, exceptions, and return type of this method. + """ + wire = q.to_wire() + (begin_time, expiration) = _compute_times(timeout) + af = dns.inet.af_for_address(where) + destination = _lltuple((where, port), af) + if sock: + cm: contextlib.AbstractAsyncContextManager = NullContext(sock) + else: + if not backend: + backend = dns.asyncbackend.get_default_backend() + stuple = _source_tuple(af, source, source_port) + if backend.datagram_connection_required(): + dtuple = (where, port) + else: + dtuple = None + cm = await backend.make_socket(af, socket.SOCK_DGRAM, 0, stuple, dtuple) + async with cm as s: + await send_udp(s, wire, destination, expiration) # pyright: ignore + (r, received_time, _) = await receive_udp( + s, # pyright: ignore + destination, + expiration, + ignore_unexpected, + one_rr_per_rrset, + q.keyring, + q.mac, + ignore_trailing, + raise_on_truncation, + ignore_errors, + q, + ) + r.time = received_time - begin_time + # We don't need to check q.is_response() if we are in ignore_errors mode + # as receive_udp() will have checked it. + if not (ignore_errors or q.is_response(r)): + raise BadResponse + return r + + +async def udp_with_fallback( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 53, + source: str | None = None, + source_port: int = 0, + ignore_unexpected: bool = False, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + udp_sock: dns.asyncbackend.DatagramSocket | None = None, + tcp_sock: dns.asyncbackend.StreamSocket | None = None, + backend: dns.asyncbackend.Backend | None = None, + ignore_errors: bool = False, +) -> Tuple[dns.message.Message, bool]: + """Return the response to the query, trying UDP first and falling back + to TCP if UDP results in a truncated response. + + *udp_sock*, a ``dns.asyncbackend.DatagramSocket``, or ``None``, + the socket to use for the UDP query. If ``None``, the default, a + socket is created. Note that if a socket is provided the *source*, + *source_port*, and *backend* are ignored for the UDP query. + + *tcp_sock*, a ``dns.asyncbackend.StreamSocket``, or ``None``, the + socket to use for the TCP query. If ``None``, the default, a + socket is created. Note that if a socket is provided *where*, + *source*, *source_port*, and *backend* are ignored for the TCP query. + + *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``, + the default, then dnspython will use the default backend. + + See :py:func:`dns.query.udp_with_fallback()` for the documentation + of the other parameters, exceptions, and return type of this + method. + """ + try: + response = await udp( + q, + where, + timeout, + port, + source, + source_port, + ignore_unexpected, + one_rr_per_rrset, + ignore_trailing, + True, + udp_sock, + backend, + ignore_errors, + ) + return (response, False) + except dns.message.Truncated: + response = await tcp( + q, + where, + timeout, + port, + source, + source_port, + one_rr_per_rrset, + ignore_trailing, + tcp_sock, + backend, + ) + return (response, True) + + +async def send_tcp( + sock: dns.asyncbackend.StreamSocket, + what: dns.message.Message | bytes, + expiration: float | None = None, +) -> Tuple[int, float]: + """Send a DNS message to the specified TCP socket. + + *sock*, a ``dns.asyncbackend.StreamSocket``. + + See :py:func:`dns.query.send_tcp()` for the documentation of the other + parameters, exceptions, and return type of this method. + """ + + if isinstance(what, dns.message.Message): + tcpmsg = what.to_wire(prepend_length=True) + else: + # copying the wire into tcpmsg is inefficient, but lets us + # avoid writev() or doing a short write that would get pushed + # onto the net + tcpmsg = len(what).to_bytes(2, "big") + what + sent_time = time.time() + await sock.sendall(tcpmsg, _timeout(expiration, sent_time)) + return (len(tcpmsg), sent_time) + + +async def _read_exactly(sock, count, expiration): + """Read the specified number of bytes from stream. Keep trying until we + either get the desired amount, or we hit EOF. + """ + s = b"" + while count > 0: + n = await sock.recv(count, _timeout(expiration)) + if n == b"": + raise EOFError("EOF") + count = count - len(n) + s = s + n + return s + + +async def receive_tcp( + sock: dns.asyncbackend.StreamSocket, + expiration: float | None = None, + one_rr_per_rrset: bool = False, + keyring: Dict[dns.name.Name, dns.tsig.Key] | None = None, + request_mac: bytes | None = b"", + ignore_trailing: bool = False, +) -> Tuple[dns.message.Message, float]: + """Read a DNS message from a TCP socket. + + *sock*, a ``dns.asyncbackend.StreamSocket``. + + See :py:func:`dns.query.receive_tcp()` for the documentation of the other + parameters, exceptions, and return type of this method. + """ + + ldata = await _read_exactly(sock, 2, expiration) + (l,) = struct.unpack("!H", ldata) + wire = await _read_exactly(sock, l, expiration) + received_time = time.time() + r = dns.message.from_wire( + wire, + keyring=keyring, + request_mac=request_mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ) + return (r, received_time) + + +async def tcp( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 53, + source: str | None = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + sock: dns.asyncbackend.StreamSocket | None = None, + backend: dns.asyncbackend.Backend | None = None, +) -> dns.message.Message: + """Return the response obtained after sending a query via TCP. + + *sock*, a ``dns.asyncbacket.StreamSocket``, or ``None``, the + socket to use for the query. If ``None``, the default, a socket + is created. Note that if a socket is provided + *where*, *port*, *source*, *source_port*, and *backend* are ignored. + + *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``, + the default, then dnspython will use the default backend. + + See :py:func:`dns.query.tcp()` for the documentation of the other + parameters, exceptions, and return type of this method. + """ + + wire = q.to_wire() + (begin_time, expiration) = _compute_times(timeout) + if sock: + # Verify that the socket is connected, as if it's not connected, + # it's not writable, and the polling in send_tcp() will time out or + # hang forever. + await sock.getpeername() + cm: contextlib.AbstractAsyncContextManager = NullContext(sock) + else: + # These are simple (address, port) pairs, not family-dependent tuples + # you pass to low-level socket code. + af = dns.inet.af_for_address(where) + stuple = _source_tuple(af, source, source_port) + dtuple = (where, port) + if not backend: + backend = dns.asyncbackend.get_default_backend() + cm = await backend.make_socket( + af, socket.SOCK_STREAM, 0, stuple, dtuple, timeout + ) + async with cm as s: + await send_tcp(s, wire, expiration) # pyright: ignore + (r, received_time) = await receive_tcp( + s, # pyright: ignore + expiration, + one_rr_per_rrset, + q.keyring, + q.mac, + ignore_trailing, + ) + r.time = received_time - begin_time + if not q.is_response(r): + raise BadResponse + return r + + +async def tls( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 853, + source: str | None = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + sock: dns.asyncbackend.StreamSocket | None = None, + backend: dns.asyncbackend.Backend | None = None, + ssl_context: ssl.SSLContext | None = None, + server_hostname: str | None = None, + verify: bool | str = True, +) -> dns.message.Message: + """Return the response obtained after sending a query via TLS. + + *sock*, an ``asyncbackend.StreamSocket``, or ``None``, the socket + to use for the query. If ``None``, the default, a socket is + created. Note that if a socket is provided, it must be a + connected SSL stream socket, and *where*, *port*, + *source*, *source_port*, *backend*, *ssl_context*, and *server_hostname* + are ignored. + + *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``, + the default, then dnspython will use the default backend. + + See :py:func:`dns.query.tls()` for the documentation of the other + parameters, exceptions, and return type of this method. + """ + (begin_time, expiration) = _compute_times(timeout) + if sock: + cm: contextlib.AbstractAsyncContextManager = NullContext(sock) + else: + if ssl_context is None: + ssl_context = make_ssl_context(verify, server_hostname is not None, ["dot"]) + af = dns.inet.af_for_address(where) + stuple = _source_tuple(af, source, source_port) + dtuple = (where, port) + if not backend: + backend = dns.asyncbackend.get_default_backend() + cm = await backend.make_socket( + af, + socket.SOCK_STREAM, + 0, + stuple, + dtuple, + timeout, + ssl_context, + server_hostname, + ) + async with cm as s: + timeout = _timeout(expiration) + response = await tcp( + q, + where, + timeout, + port, + source, + source_port, + one_rr_per_rrset, + ignore_trailing, + s, + backend, + ) + end_time = time.time() + response.time = end_time - begin_time + return response + + +def _maybe_get_resolver( + resolver: Optional["dns.asyncresolver.Resolver"], # pyright: ignore +) -> "dns.asyncresolver.Resolver": # pyright: ignore + # We need a separate method for this to avoid overriding the global + # variable "dns" with the as-yet undefined local variable "dns" + # in https(). + if resolver is None: + # pylint: disable=import-outside-toplevel,redefined-outer-name + import dns.asyncresolver + + resolver = dns.asyncresolver.Resolver() + return resolver + + +async def https( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 443, + source: str | None = None, + source_port: int = 0, # pylint: disable=W0613 + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + client: Optional["httpx.AsyncClient|dns.quic.AsyncQuicConnection"] = None, + path: str = "/dns-query", + post: bool = True, + verify: bool | str | ssl.SSLContext = True, + bootstrap_address: str | None = None, + resolver: Optional["dns.asyncresolver.Resolver"] = None, # pyright: ignore + family: int = socket.AF_UNSPEC, + http_version: HTTPVersion = HTTPVersion.DEFAULT, +) -> dns.message.Message: + """Return the response obtained after sending a query via DNS-over-HTTPS. + + *client*, a ``httpx.AsyncClient``. If provided, the client to use for + the query. + + Unlike the other dnspython async functions, a backend cannot be provided + in this function because httpx always auto-detects the async backend. + + See :py:func:`dns.query.https()` for the documentation of the other + parameters, exceptions, and return type of this method. + """ + + try: + af = dns.inet.af_for_address(where) + except ValueError: + af = None + # we bind url and then override as pyright can't figure out all paths bind. + url = where + if af is not None and dns.inet.is_address(where): + if af == socket.AF_INET: + url = f"https://{where}:{port}{path}" + elif af == socket.AF_INET6: + url = f"https://[{where}]:{port}{path}" + + extensions = {} + if bootstrap_address is None: + # pylint: disable=possibly-used-before-assignment + parsed = urllib.parse.urlparse(url) + if parsed.hostname is None: + raise ValueError("no hostname in URL") + if dns.inet.is_address(parsed.hostname): + bootstrap_address = parsed.hostname + extensions["sni_hostname"] = parsed.hostname + if parsed.port is not None: + port = parsed.port + + if http_version == HTTPVersion.H3 or ( + http_version == HTTPVersion.DEFAULT and not have_doh + ): + if bootstrap_address is None: + resolver = _maybe_get_resolver(resolver) + assert parsed.hostname is not None # pyright: ignore + answers = await resolver.resolve_name( # pyright: ignore + parsed.hostname, family # pyright: ignore + ) + bootstrap_address = random.choice(list(answers.addresses())) + if client and not isinstance( + client, dns.quic.AsyncQuicConnection + ): # pyright: ignore + raise ValueError("client parameter must be a dns.quic.AsyncQuicConnection.") + assert client is None or isinstance(client, dns.quic.AsyncQuicConnection) + return await _http3( + q, + bootstrap_address, + url, + timeout, + port, + source, + source_port, + one_rr_per_rrset, + ignore_trailing, + verify=verify, + post=post, + connection=client, + ) + + if not have_doh: + raise NoDOH # pragma: no cover + # pylint: disable=possibly-used-before-assignment + if client and not isinstance(client, httpx.AsyncClient): # pyright: ignore + raise ValueError("client parameter must be an httpx.AsyncClient") + # pylint: enable=possibly-used-before-assignment + + wire = q.to_wire() + headers = {"accept": "application/dns-message"} + + h1 = http_version in (HTTPVersion.H1, HTTPVersion.DEFAULT) + h2 = http_version in (HTTPVersion.H2, HTTPVersion.DEFAULT) + + backend = dns.asyncbackend.get_default_backend() + + if source is None: + local_address = None + local_port = 0 + else: + local_address = source + local_port = source_port + + if client: + cm: contextlib.AbstractAsyncContextManager = NullContext(client) + else: + transport = backend.get_transport_class()( + local_address=local_address, + http1=h1, + http2=h2, + verify=verify, + local_port=local_port, + bootstrap_address=bootstrap_address, + resolver=resolver, + family=family, + ) + + cm = httpx.AsyncClient( # pyright: ignore + http1=h1, http2=h2, verify=verify, transport=transport # type: ignore + ) + + async with cm as the_client: + # see https://tools.ietf.org/html/rfc8484#section-4.1.1 for DoH + # GET and POST examples + if post: + headers.update( + { + "content-type": "application/dns-message", + "content-length": str(len(wire)), + } + ) + response = await backend.wait_for( + the_client.post( # pyright: ignore + url, + headers=headers, + content=wire, + extensions=extensions, + ), + timeout, + ) + else: + wire = base64.urlsafe_b64encode(wire).rstrip(b"=") + twire = wire.decode() # httpx does a repr() if we give it bytes + response = await backend.wait_for( + the_client.get( # pyright: ignore + url, + headers=headers, + params={"dns": twire}, + extensions=extensions, + ), + timeout, + ) + + # see https://tools.ietf.org/html/rfc8484#section-4.2.1 for info about DoH + # status codes + if response.status_code < 200 or response.status_code > 299: + raise ValueError( + f"{where} responded with status code {response.status_code}" + f"\nResponse body: {response.content!r}" + ) + r = dns.message.from_wire( + response.content, + keyring=q.keyring, + request_mac=q.request_mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ) + r.time = response.elapsed.total_seconds() + if not q.is_response(r): + raise BadResponse + return r + + +async def _http3( + q: dns.message.Message, + where: str, + url: str, + timeout: float | None = None, + port: int = 443, + source: str | None = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + verify: bool | str | ssl.SSLContext = True, + backend: dns.asyncbackend.Backend | None = None, + post: bool = True, + connection: dns.quic.AsyncQuicConnection | None = None, +) -> dns.message.Message: + if not dns.quic.have_quic: + raise NoDOH("DNS-over-HTTP3 is not available.") # pragma: no cover + + url_parts = urllib.parse.urlparse(url) + hostname = url_parts.hostname + assert hostname is not None + if url_parts.port is not None: + port = url_parts.port + + q.id = 0 + wire = q.to_wire() + the_connection: dns.quic.AsyncQuicConnection + if connection: + cfactory = dns.quic.null_factory + mfactory = dns.quic.null_factory + else: + (cfactory, mfactory) = dns.quic.factories_for_backend(backend) + + async with cfactory() as context: + async with mfactory( + context, verify_mode=verify, server_name=hostname, h3=True + ) as the_manager: + if connection: + the_connection = connection + else: + the_connection = the_manager.connect( # pyright: ignore + where, port, source, source_port + ) + (start, expiration) = _compute_times(timeout) + stream = await the_connection.make_stream(timeout) # pyright: ignore + async with stream: + # note that send_h3() does not need await + stream.send_h3(url, wire, post) + wire = await stream.receive(_remaining(expiration)) + _check_status(stream.headers(), where, wire) + finish = time.time() + r = dns.message.from_wire( + wire, + keyring=q.keyring, + request_mac=q.request_mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ) + r.time = max(finish - start, 0.0) + if not q.is_response(r): + raise BadResponse + return r + + +async def quic( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 853, + source: str | None = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + connection: dns.quic.AsyncQuicConnection | None = None, + verify: bool | str = True, + backend: dns.asyncbackend.Backend | None = None, + hostname: str | None = None, + server_hostname: str | None = None, +) -> dns.message.Message: + """Return the response obtained after sending an asynchronous query via + DNS-over-QUIC. + + *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``, + the default, then dnspython will use the default backend. + + See :py:func:`dns.query.quic()` for the documentation of the other + parameters, exceptions, and return type of this method. + """ + + if not dns.quic.have_quic: + raise NoDOQ("DNS-over-QUIC is not available.") # pragma: no cover + + if server_hostname is not None and hostname is None: + hostname = server_hostname + + q.id = 0 + wire = q.to_wire() + the_connection: dns.quic.AsyncQuicConnection + if connection: + cfactory = dns.quic.null_factory + mfactory = dns.quic.null_factory + the_connection = connection + else: + (cfactory, mfactory) = dns.quic.factories_for_backend(backend) + + async with cfactory() as context: + async with mfactory( + context, + verify_mode=verify, + server_name=server_hostname, + ) as the_manager: + if not connection: + the_connection = the_manager.connect( # pyright: ignore + where, port, source, source_port + ) + (start, expiration) = _compute_times(timeout) + stream = await the_connection.make_stream(timeout) # pyright: ignore + async with stream: + await stream.send(wire, True) + wire = await stream.receive(_remaining(expiration)) + finish = time.time() + r = dns.message.from_wire( + wire, + keyring=q.keyring, + request_mac=q.request_mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ) + r.time = max(finish - start, 0.0) + if not q.is_response(r): + raise BadResponse + return r + + +async def _inbound_xfr( + txn_manager: dns.transaction.TransactionManager, + s: dns.asyncbackend.Socket, + query: dns.message.Message, + serial: int | None, + timeout: float | None, + expiration: float, +) -> Any: + """Given a socket, does the zone transfer.""" + rdtype = query.question[0].rdtype + is_ixfr = rdtype == dns.rdatatype.IXFR + origin = txn_manager.from_wire_origin() + wire = query.to_wire() + is_udp = s.type == socket.SOCK_DGRAM + if is_udp: + udp_sock = cast(dns.asyncbackend.DatagramSocket, s) + await udp_sock.sendto(wire, None, _timeout(expiration)) + else: + tcp_sock = cast(dns.asyncbackend.StreamSocket, s) + tcpmsg = struct.pack("!H", len(wire)) + wire + await tcp_sock.sendall(tcpmsg, expiration) + with dns.xfr.Inbound(txn_manager, rdtype, serial, is_udp) as inbound: + done = False + tsig_ctx = None + r: dns.message.Message | None = None + while not done: + (_, mexpiration) = _compute_times(timeout) + if mexpiration is None or ( + expiration is not None and mexpiration > expiration + ): + mexpiration = expiration + if is_udp: + timeout = _timeout(mexpiration) + (rwire, _) = await udp_sock.recvfrom(65535, timeout) # pyright: ignore + else: + ldata = await _read_exactly(tcp_sock, 2, mexpiration) # pyright: ignore + (l,) = struct.unpack("!H", ldata) + rwire = await _read_exactly(tcp_sock, l, mexpiration) # pyright: ignore + r = dns.message.from_wire( + rwire, + keyring=query.keyring, + request_mac=query.mac, + xfr=True, + origin=origin, + tsig_ctx=tsig_ctx, + multi=(not is_udp), + one_rr_per_rrset=is_ixfr, + ) + done = inbound.process_message(r) + yield r + tsig_ctx = r.tsig_ctx + if query.keyring and r is not None and not r.had_tsig: + raise dns.exception.FormError("missing TSIG") + + +async def inbound_xfr( + where: str, + txn_manager: dns.transaction.TransactionManager, + query: dns.message.Message | None = None, + port: int = 53, + timeout: float | None = None, + lifetime: float | None = None, + source: str | None = None, + source_port: int = 0, + udp_mode: UDPMode = UDPMode.NEVER, + backend: dns.asyncbackend.Backend | None = None, +) -> None: + """Conduct an inbound transfer and apply it via a transaction from the + txn_manager. + + *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``, + the default, then dnspython will use the default backend. + + See :py:func:`dns.query.inbound_xfr()` for the documentation of + the other parameters, exceptions, and return type of this method. + """ + if query is None: + (query, serial) = dns.xfr.make_query(txn_manager) + else: + serial = dns.xfr.extract_serial_from_query(query) + af = dns.inet.af_for_address(where) + stuple = _source_tuple(af, source, source_port) + dtuple = (where, port) + if not backend: + backend = dns.asyncbackend.get_default_backend() + (_, expiration) = _compute_times(lifetime) + if query.question[0].rdtype == dns.rdatatype.IXFR and udp_mode != UDPMode.NEVER: + s = await backend.make_socket( + af, socket.SOCK_DGRAM, 0, stuple, dtuple, _timeout(expiration) + ) + async with s: + try: + async for _ in _inbound_xfr( # pyright: ignore + txn_manager, + s, + query, + serial, + timeout, + expiration, # pyright: ignore + ): + pass + return + except dns.xfr.UseTCP: + if udp_mode == UDPMode.ONLY: + raise + + s = await backend.make_socket( + af, socket.SOCK_STREAM, 0, stuple, dtuple, _timeout(expiration) + ) + async with s: + async for _ in _inbound_xfr( # pyright: ignore + txn_manager, s, query, serial, timeout, expiration # pyright: ignore + ): + pass diff --git a/netdeploy/lib/python3.11/site-packages/dns/asyncresolver.py b/netdeploy/lib/python3.11/site-packages/dns/asyncresolver.py new file mode 100644 index 0000000..6f8c69f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/asyncresolver.py @@ -0,0 +1,478 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Asynchronous DNS stub resolver.""" + +import socket +import time +from typing import Any, Dict, List + +import dns._ddr +import dns.asyncbackend +import dns.asyncquery +import dns.exception +import dns.inet +import dns.name +import dns.nameserver +import dns.query +import dns.rdataclass +import dns.rdatatype +import dns.resolver # lgtm[py/import-and-import-from] +import dns.reversename + +# import some resolver symbols for brevity +from dns.resolver import NXDOMAIN, NoAnswer, NoRootSOA, NotAbsolute + +# for indentation purposes below +_udp = dns.asyncquery.udp +_tcp = dns.asyncquery.tcp + + +class Resolver(dns.resolver.BaseResolver): + """Asynchronous DNS stub resolver.""" + + async def resolve( + self, + qname: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A, + rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + tcp: bool = False, + source: str | None = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: float | None = None, + search: bool | None = None, + backend: dns.asyncbackend.Backend | None = None, + ) -> dns.resolver.Answer: + """Query nameservers asynchronously to find the answer to the question. + + *backend*, a ``dns.asyncbackend.Backend``, or ``None``. If ``None``, + the default, then dnspython will use the default backend. + + See :py:func:`dns.resolver.Resolver.resolve()` for the + documentation of the other parameters, exceptions, and return + type of this method. + """ + + resolution = dns.resolver._Resolution( + self, qname, rdtype, rdclass, tcp, raise_on_no_answer, search + ) + if not backend: + backend = dns.asyncbackend.get_default_backend() + start = time.time() + while True: + (request, answer) = resolution.next_request() + # Note we need to say "if answer is not None" and not just + # "if answer" because answer implements __len__, and python + # will call that. We want to return if we have an answer + # object, including in cases where its length is 0. + if answer is not None: + # cache hit! + return answer + assert request is not None # needed for type checking + done = False + while not done: + (nameserver, tcp, backoff) = resolution.next_nameserver() + if backoff: + await backend.sleep(backoff) + timeout = self._compute_timeout(start, lifetime, resolution.errors) + try: + response = await nameserver.async_query( + request, + timeout=timeout, + source=source, + source_port=source_port, + max_size=tcp, + backend=backend, + ) + except Exception as ex: + (_, done) = resolution.query_result(None, ex) + continue + (answer, done) = resolution.query_result(response, None) + # Note we need to say "if answer is not None" and not just + # "if answer" because answer implements __len__, and python + # will call that. We want to return if we have an answer + # object, including in cases where its length is 0. + if answer is not None: + return answer + + async def resolve_address( + self, ipaddr: str, *args: Any, **kwargs: Any + ) -> dns.resolver.Answer: + """Use an asynchronous resolver to run a reverse query for PTR + records. + + This utilizes the resolve() method to perform a PTR lookup on the + specified IP address. + + *ipaddr*, a ``str``, the IPv4 or IPv6 address you want to get + the PTR record for. + + All other arguments that can be passed to the resolve() function + except for rdtype and rdclass are also supported by this + function. + + """ + # We make a modified kwargs for type checking happiness, as otherwise + # we get a legit warning about possibly having rdtype and rdclass + # in the kwargs more than once. + modified_kwargs: Dict[str, Any] = {} + modified_kwargs.update(kwargs) + modified_kwargs["rdtype"] = dns.rdatatype.PTR + modified_kwargs["rdclass"] = dns.rdataclass.IN + return await self.resolve( + dns.reversename.from_address(ipaddr), *args, **modified_kwargs + ) + + async def resolve_name( + self, + name: dns.name.Name | str, + family: int = socket.AF_UNSPEC, + **kwargs: Any, + ) -> dns.resolver.HostAnswers: + """Use an asynchronous resolver to query for address records. + + This utilizes the resolve() method to perform A and/or AAAA lookups on + the specified name. + + *qname*, a ``dns.name.Name`` or ``str``, the name to resolve. + + *family*, an ``int``, the address family. If socket.AF_UNSPEC + (the default), both A and AAAA records will be retrieved. + + All other arguments that can be passed to the resolve() function + except for rdtype and rdclass are also supported by this + function. + """ + # We make a modified kwargs for type checking happiness, as otherwise + # we get a legit warning about possibly having rdtype and rdclass + # in the kwargs more than once. + modified_kwargs: Dict[str, Any] = {} + modified_kwargs.update(kwargs) + modified_kwargs.pop("rdtype", None) + modified_kwargs["rdclass"] = dns.rdataclass.IN + + if family == socket.AF_INET: + v4 = await self.resolve(name, dns.rdatatype.A, **modified_kwargs) + return dns.resolver.HostAnswers.make(v4=v4) + elif family == socket.AF_INET6: + v6 = await self.resolve(name, dns.rdatatype.AAAA, **modified_kwargs) + return dns.resolver.HostAnswers.make(v6=v6) + elif family != socket.AF_UNSPEC: + raise NotImplementedError(f"unknown address family {family}") + + raise_on_no_answer = modified_kwargs.pop("raise_on_no_answer", True) + lifetime = modified_kwargs.pop("lifetime", None) + start = time.time() + v6 = await self.resolve( + name, + dns.rdatatype.AAAA, + raise_on_no_answer=False, + lifetime=self._compute_timeout(start, lifetime), + **modified_kwargs, + ) + # Note that setting name ensures we query the same name + # for A as we did for AAAA. (This is just in case search lists + # are active by default in the resolver configuration and + # we might be talking to a server that says NXDOMAIN when it + # wants to say NOERROR no data. + name = v6.qname + v4 = await self.resolve( + name, + dns.rdatatype.A, + raise_on_no_answer=False, + lifetime=self._compute_timeout(start, lifetime), + **modified_kwargs, + ) + answers = dns.resolver.HostAnswers.make( + v6=v6, v4=v4, add_empty=not raise_on_no_answer + ) + if not answers: + raise NoAnswer(response=v6.response) + return answers + + # pylint: disable=redefined-outer-name + + async def canonical_name(self, name: dns.name.Name | str) -> dns.name.Name: + """Determine the canonical name of *name*. + + The canonical name is the name the resolver uses for queries + after all CNAME and DNAME renamings have been applied. + + *name*, a ``dns.name.Name`` or ``str``, the query name. + + This method can raise any exception that ``resolve()`` can + raise, other than ``dns.resolver.NoAnswer`` and + ``dns.resolver.NXDOMAIN``. + + Returns a ``dns.name.Name``. + """ + try: + answer = await self.resolve(name, raise_on_no_answer=False) + canonical_name = answer.canonical_name + except dns.resolver.NXDOMAIN as e: + canonical_name = e.canonical_name + return canonical_name + + async def try_ddr(self, lifetime: float = 5.0) -> None: + """Try to update the resolver's nameservers using Discovery of Designated + Resolvers (DDR). If successful, the resolver will subsequently use + DNS-over-HTTPS or DNS-over-TLS for future queries. + + *lifetime*, a float, is the maximum time to spend attempting DDR. The default + is 5 seconds. + + If the SVCB query is successful and results in a non-empty list of nameservers, + then the resolver's nameservers are set to the returned servers in priority + order. + + The current implementation does not use any address hints from the SVCB record, + nor does it resolve addresses for the SCVB target name, rather it assumes that + the bootstrap nameserver will always be one of the addresses and uses it. + A future revision to the code may offer fuller support. The code verifies that + the bootstrap nameserver is in the Subject Alternative Name field of the + TLS certficate. + """ + try: + expiration = time.time() + lifetime + answer = await self.resolve( + dns._ddr._local_resolver_name, "svcb", lifetime=lifetime + ) + timeout = dns.query._remaining(expiration) + nameservers = await dns._ddr._get_nameservers_async(answer, timeout) + if len(nameservers) > 0: + self.nameservers = nameservers + except Exception: + pass + + +default_resolver = None + + +def get_default_resolver() -> Resolver: + """Get the default asynchronous resolver, initializing it if necessary.""" + if default_resolver is None: + reset_default_resolver() + assert default_resolver is not None + return default_resolver + + +def reset_default_resolver() -> None: + """Re-initialize default asynchronous resolver. + + Note that the resolver configuration (i.e. /etc/resolv.conf on UNIX + systems) will be re-read immediately. + """ + + global default_resolver + default_resolver = Resolver() + + +async def resolve( + qname: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A, + rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + tcp: bool = False, + source: str | None = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: float | None = None, + search: bool | None = None, + backend: dns.asyncbackend.Backend | None = None, +) -> dns.resolver.Answer: + """Query nameservers asynchronously to find the answer to the question. + + This is a convenience function that uses the default resolver + object to make the query. + + See :py:func:`dns.asyncresolver.Resolver.resolve` for more + information on the parameters. + """ + + return await get_default_resolver().resolve( + qname, + rdtype, + rdclass, + tcp, + source, + raise_on_no_answer, + source_port, + lifetime, + search, + backend, + ) + + +async def resolve_address( + ipaddr: str, *args: Any, **kwargs: Any +) -> dns.resolver.Answer: + """Use a resolver to run a reverse query for PTR records. + + See :py:func:`dns.asyncresolver.Resolver.resolve_address` for more + information on the parameters. + """ + + return await get_default_resolver().resolve_address(ipaddr, *args, **kwargs) + + +async def resolve_name( + name: dns.name.Name | str, family: int = socket.AF_UNSPEC, **kwargs: Any +) -> dns.resolver.HostAnswers: + """Use a resolver to asynchronously query for address records. + + See :py:func:`dns.asyncresolver.Resolver.resolve_name` for more + information on the parameters. + """ + + return await get_default_resolver().resolve_name(name, family, **kwargs) + + +async def canonical_name(name: dns.name.Name | str) -> dns.name.Name: + """Determine the canonical name of *name*. + + See :py:func:`dns.resolver.Resolver.canonical_name` for more + information on the parameters and possible exceptions. + """ + + return await get_default_resolver().canonical_name(name) + + +async def try_ddr(timeout: float = 5.0) -> None: + """Try to update the default resolver's nameservers using Discovery of Designated + Resolvers (DDR). If successful, the resolver will subsequently use + DNS-over-HTTPS or DNS-over-TLS for future queries. + + See :py:func:`dns.resolver.Resolver.try_ddr` for more information. + """ + return await get_default_resolver().try_ddr(timeout) + + +async def zone_for_name( + name: dns.name.Name | str, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + tcp: bool = False, + resolver: Resolver | None = None, + backend: dns.asyncbackend.Backend | None = None, +) -> dns.name.Name: + """Find the name of the zone which contains the specified name. + + See :py:func:`dns.resolver.Resolver.zone_for_name` for more + information on the parameters and possible exceptions. + """ + + if isinstance(name, str): + name = dns.name.from_text(name, dns.name.root) + if resolver is None: + resolver = get_default_resolver() + if not name.is_absolute(): + raise NotAbsolute(name) + while True: + try: + answer = await resolver.resolve( + name, dns.rdatatype.SOA, rdclass, tcp, backend=backend + ) + assert answer.rrset is not None + if answer.rrset.name == name: + return name + # otherwise we were CNAMEd or DNAMEd and need to look higher + except (NXDOMAIN, NoAnswer): + pass + try: + name = name.parent() + except dns.name.NoParent: # pragma: no cover + raise NoRootSOA + + +async def make_resolver_at( + where: dns.name.Name | str, + port: int = 53, + family: int = socket.AF_UNSPEC, + resolver: Resolver | None = None, +) -> Resolver: + """Make a stub resolver using the specified destination as the full resolver. + + *where*, a ``dns.name.Name`` or ``str`` the domain name or IP address of the + full resolver. + + *port*, an ``int``, the port to use. If not specified, the default is 53. + + *family*, an ``int``, the address family to use. This parameter is used if + *where* is not an address. The default is ``socket.AF_UNSPEC`` in which case + the first address returned by ``resolve_name()`` will be used, otherwise the + first address of the specified family will be used. + + *resolver*, a ``dns.asyncresolver.Resolver`` or ``None``, the resolver to use for + resolution of hostnames. If not specified, the default resolver will be used. + + Returns a ``dns.resolver.Resolver`` or raises an exception. + """ + if resolver is None: + resolver = get_default_resolver() + nameservers: List[str | dns.nameserver.Nameserver] = [] + if isinstance(where, str) and dns.inet.is_address(where): + nameservers.append(dns.nameserver.Do53Nameserver(where, port)) + else: + answers = await resolver.resolve_name(where, family) + for address in answers.addresses(): + nameservers.append(dns.nameserver.Do53Nameserver(address, port)) + res = Resolver(configure=False) + res.nameservers = nameservers + return res + + +async def resolve_at( + where: dns.name.Name | str, + qname: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A, + rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + tcp: bool = False, + source: str | None = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: float | None = None, + search: bool | None = None, + backend: dns.asyncbackend.Backend | None = None, + port: int = 53, + family: int = socket.AF_UNSPEC, + resolver: Resolver | None = None, +) -> dns.resolver.Answer: + """Query nameservers to find the answer to the question. + + This is a convenience function that calls ``dns.asyncresolver.make_resolver_at()`` + to make a resolver, and then uses it to resolve the query. + + See ``dns.asyncresolver.Resolver.resolve`` for more information on the resolution + parameters, and ``dns.asyncresolver.make_resolver_at`` for information about the + resolver parameters *where*, *port*, *family*, and *resolver*. + + If making more than one query, it is more efficient to call + ``dns.asyncresolver.make_resolver_at()`` and then use that resolver for the queries + instead of calling ``resolve_at()`` multiple times. + """ + res = await make_resolver_at(where, port, family, resolver) + return await res.resolve( + qname, + rdtype, + rdclass, + tcp, + source, + raise_on_no_answer, + source_port, + lifetime, + search, + backend, + ) diff --git a/netdeploy/lib/python3.11/site-packages/dns/btree.py b/netdeploy/lib/python3.11/site-packages/dns/btree.py new file mode 100644 index 0000000..12da9f5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/btree.py @@ -0,0 +1,850 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +""" +A BTree in the style of Cormen, Leiserson, and Rivest's "Algorithms" book, with +copy-on-write node updates, cursors, and optional space optimization for mostly-in-order +insertion. +""" + +from collections.abc import MutableMapping, MutableSet +from typing import Any, Callable, Generic, Optional, Tuple, TypeVar, cast + +DEFAULT_T = 127 + +KT = TypeVar("KT") # the type of a key in Element + + +class Element(Generic[KT]): + """All items stored in the BTree are Elements.""" + + def key(self) -> KT: + """The key for this element; the returned type must implement comparison.""" + raise NotImplementedError # pragma: no cover + + +ET = TypeVar("ET", bound=Element) # the type of a value in a _KV + + +def _MIN(t: int) -> int: + """The minimum number of keys in a non-root node for a BTree with the specified + ``t`` + """ + return t - 1 + + +def _MAX(t: int) -> int: + """The maximum number of keys in node for a BTree with the specified ``t``""" + return 2 * t - 1 + + +class _Creator: + """A _Creator class instance is used as a unique id for the BTree which created + a node. + + We use a dedicated creator rather than just a BTree reference to avoid circularity + that would complicate GC. + """ + + def __str__(self): # pragma: no cover + return f"{id(self):x}" + + +class _Node(Generic[KT, ET]): + """A Node in the BTree. + + A Node (leaf or internal) of the BTree. + """ + + __slots__ = ["t", "creator", "is_leaf", "elts", "children"] + + def __init__(self, t: int, creator: _Creator, is_leaf: bool): + assert t >= 3 + self.t = t + self.creator = creator + self.is_leaf = is_leaf + self.elts: list[ET] = [] + self.children: list[_Node[KT, ET]] = [] + + def is_maximal(self) -> bool: + """Does this node have the maximal number of keys?""" + assert len(self.elts) <= _MAX(self.t) + return len(self.elts) == _MAX(self.t) + + def is_minimal(self) -> bool: + """Does this node have the minimal number of keys?""" + assert len(self.elts) >= _MIN(self.t) + return len(self.elts) == _MIN(self.t) + + def search_in_node(self, key: KT) -> tuple[int, bool]: + """Get the index of the ``Element`` matching ``key`` or the index of its + least successor. + + Returns a tuple of the index and an ``equal`` boolean that is ``True`` iff. + the key was found. + """ + l = len(self.elts) + if l > 0 and key > self.elts[l - 1].key(): + # This is optimizing near in-order insertion. + return l, False + l = 0 + i = len(self.elts) + r = i - 1 + equal = False + while l <= r: + m = (l + r) // 2 + k = self.elts[m].key() + if key == k: + i = m + equal = True + break + elif key < k: + i = m + r = m - 1 + else: + l = m + 1 + return i, equal + + def maybe_cow_child(self, index: int) -> "_Node[KT, ET]": + assert not self.is_leaf + child = self.children[index] + cloned = child.maybe_cow(self.creator) + if cloned: + self.children[index] = cloned + return cloned + else: + return child + + def _get_node(self, key: KT) -> Tuple[Optional["_Node[KT, ET]"], int]: + """Get the node associated with key and its index, doing + copy-on-write if we have to descend. + + Returns a tuple of the node and the index, or the tuple ``(None, 0)`` + if the key was not found. + """ + i, equal = self.search_in_node(key) + if equal: + return (self, i) + elif self.is_leaf: + return (None, 0) + else: + child = self.maybe_cow_child(i) + return child._get_node(key) + + def get(self, key: KT) -> ET | None: + """Get the element associated with *key* or return ``None``""" + i, equal = self.search_in_node(key) + if equal: + return self.elts[i] + elif self.is_leaf: + return None + else: + return self.children[i].get(key) + + def optimize_in_order_insertion(self, index: int) -> None: + """Try to minimize the number of Nodes in a BTree where the insertion + is done in-order or close to it, by stealing as much as we can from our + right sibling. + + If we don't do this, then an in-order insertion will produce a BTree + where most of the nodes are minimal. + """ + if index == 0: + return + left = self.children[index - 1] + if len(left.elts) == _MAX(self.t): + return + left = self.maybe_cow_child(index - 1) + while len(left.elts) < _MAX(self.t): + if not left.try_right_steal(self, index - 1): + break + + def insert_nonfull(self, element: ET, in_order: bool) -> ET | None: + assert not self.is_maximal() + while True: + key = element.key() + i, equal = self.search_in_node(key) + if equal: + # replace + old = self.elts[i] + self.elts[i] = element + return old + elif self.is_leaf: + self.elts.insert(i, element) + return None + else: + child = self.maybe_cow_child(i) + if child.is_maximal(): + self.adopt(*child.split()) + # Splitting might result in our target moving to us, so + # search again. + continue + oelt = child.insert_nonfull(element, in_order) + if in_order: + self.optimize_in_order_insertion(i) + return oelt + + def split(self) -> tuple["_Node[KT, ET]", ET, "_Node[KT, ET]"]: + """Split a maximal node into two minimal ones and a central element.""" + assert self.is_maximal() + right = self.__class__(self.t, self.creator, self.is_leaf) + right.elts = list(self.elts[_MIN(self.t) + 1 :]) + middle = self.elts[_MIN(self.t)] + self.elts = list(self.elts[: _MIN(self.t)]) + if not self.is_leaf: + right.children = list(self.children[_MIN(self.t) + 1 :]) + self.children = list(self.children[: _MIN(self.t) + 1]) + return self, middle, right + + def try_left_steal(self, parent: "_Node[KT, ET]", index: int) -> bool: + """Try to steal from this Node's left sibling for balancing purposes. + + Returns ``True`` if the theft was successful, or ``False`` if not. + """ + if index != 0: + left = parent.children[index - 1] + if not left.is_minimal(): + left = parent.maybe_cow_child(index - 1) + elt = parent.elts[index - 1] + parent.elts[index - 1] = left.elts.pop() + self.elts.insert(0, elt) + if not left.is_leaf: + assert not self.is_leaf + child = left.children.pop() + self.children.insert(0, child) + return True + return False + + def try_right_steal(self, parent: "_Node[KT, ET]", index: int) -> bool: + """Try to steal from this Node's right sibling for balancing purposes. + + Returns ``True`` if the theft was successful, or ``False`` if not. + """ + if index + 1 < len(parent.children): + right = parent.children[index + 1] + if not right.is_minimal(): + right = parent.maybe_cow_child(index + 1) + elt = parent.elts[index] + parent.elts[index] = right.elts.pop(0) + self.elts.append(elt) + if not right.is_leaf: + assert not self.is_leaf + child = right.children.pop(0) + self.children.append(child) + return True + return False + + def adopt(self, left: "_Node[KT, ET]", middle: ET, right: "_Node[KT, ET]") -> None: + """Adopt left, middle, and right into our Node (which must not be maximal, + and which must not be a leaf). In the case were we are not the new root, + then the left child must already be in the Node.""" + assert not self.is_maximal() + assert not self.is_leaf + key = middle.key() + i, equal = self.search_in_node(key) + assert not equal + self.elts.insert(i, middle) + if len(self.children) == 0: + # We are the new root + self.children = [left, right] + else: + assert self.children[i] == left + self.children.insert(i + 1, right) + + def merge(self, parent: "_Node[KT, ET]", index: int) -> None: + """Merge this node's parent and its right sibling into this node.""" + right = parent.children.pop(index + 1) + self.elts.append(parent.elts.pop(index)) + self.elts.extend(right.elts) + if not self.is_leaf: + self.children.extend(right.children) + + def minimum(self) -> ET: + """The least element in this subtree.""" + if self.is_leaf: + return self.elts[0] + else: + return self.children[0].minimum() + + def maximum(self) -> ET: + """The greatest element in this subtree.""" + if self.is_leaf: + return self.elts[-1] + else: + return self.children[-1].maximum() + + def balance(self, parent: "_Node[KT, ET]", index: int) -> None: + """This Node is minimal, and we want to make it non-minimal so we can delete. + We try to steal from our siblings, and if that doesn't work we will merge + with one of them.""" + assert not parent.is_leaf + if self.try_left_steal(parent, index): + return + if self.try_right_steal(parent, index): + return + # Stealing didn't work, so both siblings must be minimal. + if index == 0: + # We are the left-most node so merge with our right sibling. + self.merge(parent, index) + else: + # Have our left sibling merge with us. This lets us only have "merge right" + # code. + left = parent.maybe_cow_child(index - 1) + left.merge(parent, index - 1) + + def delete( + self, key: KT, parent: Optional["_Node[KT, ET]"], exact: ET | None + ) -> ET | None: + """Delete an element matching *key* if it exists. If *exact* is not ``None`` + then it must be an exact match with that element. The Node must not be + minimal unless it is the root.""" + assert parent is None or not self.is_minimal() + i, equal = self.search_in_node(key) + original_key = None + if equal: + # Note we use "is" here as we meant "exactly this object". + if exact is not None and self.elts[i] is not exact: + raise ValueError("exact delete did not match existing elt") + if self.is_leaf: + return self.elts.pop(i) + # Note we need to ensure exact is None going forward as we've + # already checked exactness and are about to change our target key + # to the least successor. + exact = None + original_key = key + least_successor = self.children[i + 1].minimum() + key = least_successor.key() + i = i + 1 + if self.is_leaf: + # No match + if exact is not None: + raise ValueError("exact delete had no match") + return None + # recursively delete in the appropriate child + child = self.maybe_cow_child(i) + if child.is_minimal(): + child.balance(self, i) + # Things may have moved. + i, equal = self.search_in_node(key) + assert not equal + child = self.children[i] + assert not child.is_minimal() + elt = child.delete(key, self, exact) + if original_key is not None: + node, i = self._get_node(original_key) + assert node is not None + assert elt is not None + oelt = node.elts[i] + node.elts[i] = elt + elt = oelt + return elt + + def visit_in_order(self, visit: Callable[[ET], None]) -> None: + """Call *visit* on all of the elements in order.""" + for i, elt in enumerate(self.elts): + if not self.is_leaf: + self.children[i].visit_in_order(visit) + visit(elt) + if not self.is_leaf: + self.children[-1].visit_in_order(visit) + + def _visit_preorder_by_node(self, visit: Callable[["_Node[KT, ET]"], None]) -> None: + """Visit nodes in preorder. This method is only used for testing.""" + visit(self) + if not self.is_leaf: + for child in self.children: + child._visit_preorder_by_node(visit) + + def maybe_cow(self, creator: _Creator) -> Optional["_Node[KT, ET]"]: + """Return a clone of this Node if it was not created by *creator*, or ``None`` + otherwise (i.e. copy for copy-on-write if we haven't already copied it).""" + if self.creator is not creator: + return self.clone(creator) + else: + return None + + def clone(self, creator: _Creator) -> "_Node[KT, ET]": + """Make a shallow-copy duplicate of this node.""" + cloned = self.__class__(self.t, creator, self.is_leaf) + cloned.elts.extend(self.elts) + if not self.is_leaf: + cloned.children.extend(self.children) + return cloned + + def __str__(self): # pragma: no cover + if not self.is_leaf: + children = " " + " ".join([f"{id(c):x}" for c in self.children]) + else: + children = "" + return f"{id(self):x} {self.creator} {self.elts}{children}" + + +class Cursor(Generic[KT, ET]): + """A seekable cursor for a BTree. + + If you are going to use a cursor on a mutable BTree, you should use it + in a ``with`` block so that any mutations of the BTree automatically park + the cursor. + """ + + def __init__(self, btree: "BTree[KT, ET]"): + self.btree = btree + self.current_node: _Node | None = None + # The current index is the element index within the current node, or + # if there is no current node then it is 0 on the left boundary and 1 + # on the right boundary. + self.current_index: int = 0 + self.recurse = False + self.increasing = True + self.parents: list[tuple[_Node, int]] = [] + self.parked = False + self.parking_key: KT | None = None + self.parking_key_read = False + + def _seek_least(self) -> None: + # seek to the least value in the subtree beneath the current index of the + # current node + assert self.current_node is not None + while not self.current_node.is_leaf: + self.parents.append((self.current_node, self.current_index)) + self.current_node = self.current_node.children[self.current_index] + assert self.current_node is not None + self.current_index = 0 + + def _seek_greatest(self) -> None: + # seek to the greatest value in the subtree beneath the current index of the + # current node + assert self.current_node is not None + while not self.current_node.is_leaf: + self.parents.append((self.current_node, self.current_index)) + self.current_node = self.current_node.children[self.current_index] + assert self.current_node is not None + self.current_index = len(self.current_node.elts) + + def park(self): + """Park the cursor. + + A cursor must be "parked" before mutating the BTree to avoid undefined behavior. + Cursors created in a ``with`` block register with their BTree and will park + automatically. Note that a parked cursor may not observe some changes made when + it is parked; for example a cursor being iterated with next() will not see items + inserted before its current position. + """ + if not self.parked: + self.parked = True + + def _maybe_unpark(self): + if self.parked: + if self.parking_key is not None: + # remember our increasing hint, as seeking might change it + increasing = self.increasing + if self.parking_key_read: + # We've already returned the parking key, so we want to be before it + # if decreasing and after it if increasing. + before = not self.increasing + else: + # We haven't returned the parking key, so we've parked right + # after seeking or are on a boundary. Either way, the before + # hint we want is the value of self.increasing. + before = self.increasing + self.seek(self.parking_key, before) + self.increasing = increasing # might have been altered by seek() + self.parked = False + self.parking_key = None + + def prev(self) -> ET | None: + """Get the previous element, or return None if on the left boundary.""" + self._maybe_unpark() + self.parking_key = None + if self.current_node is None: + # on a boundary + if self.current_index == 0: + # left boundary, there is no prev + return None + else: + assert self.current_index == 1 + # right boundary; seek to the actual boundary + # so we can do a prev() + self.current_node = self.btree.root + self.current_index = len(self.btree.root.elts) + self._seek_greatest() + while True: + if self.recurse: + if not self.increasing: + # We only want to recurse if we are continuing in the decreasing + # direction. + self._seek_greatest() + self.recurse = False + self.increasing = False + self.current_index -= 1 + if self.current_index >= 0: + elt = self.current_node.elts[self.current_index] + if not self.current_node.is_leaf: + self.recurse = True + self.parking_key = elt.key() + self.parking_key_read = True + return elt + else: + if len(self.parents) > 0: + self.current_node, self.current_index = self.parents.pop() + else: + self.current_node = None + self.current_index = 0 + return None + + def next(self) -> ET | None: + """Get the next element, or return None if on the right boundary.""" + self._maybe_unpark() + self.parking_key = None + if self.current_node is None: + # on a boundary + if self.current_index == 1: + # right boundary, there is no next + return None + else: + assert self.current_index == 0 + # left boundary; seek to the actual boundary + # so we can do a next() + self.current_node = self.btree.root + self.current_index = 0 + self._seek_least() + while True: + if self.recurse: + if self.increasing: + # We only want to recurse if we are continuing in the increasing + # direction. + self._seek_least() + self.recurse = False + self.increasing = True + if self.current_index < len(self.current_node.elts): + elt = self.current_node.elts[self.current_index] + self.current_index += 1 + if not self.current_node.is_leaf: + self.recurse = True + self.parking_key = elt.key() + self.parking_key_read = True + return elt + else: + if len(self.parents) > 0: + self.current_node, self.current_index = self.parents.pop() + else: + self.current_node = None + self.current_index = 1 + return None + + def _adjust_for_before(self, before: bool, i: int) -> None: + if before: + self.current_index = i + else: + self.current_index = i + 1 + + def seek(self, key: KT, before: bool = True) -> None: + """Seek to the specified key. + + If *before* is ``True`` (the default) then the cursor is positioned just + before *key* if it exists, or before its least successor if it doesn't. A + subsequent next() will retrieve this value. If *before* is ``False``, then + the cursor is positioned just after *key* if it exists, or its greatest + precessessor if it doesn't. A subsequent prev() will return this value. + """ + self.current_node = self.btree.root + assert self.current_node is not None + self.recurse = False + self.parents = [] + self.increasing = before + self.parked = False + self.parking_key = key + self.parking_key_read = False + while not self.current_node.is_leaf: + i, equal = self.current_node.search_in_node(key) + if equal: + self._adjust_for_before(before, i) + if before: + self._seek_greatest() + else: + self._seek_least() + return + self.parents.append((self.current_node, i)) + self.current_node = self.current_node.children[i] + assert self.current_node is not None + i, equal = self.current_node.search_in_node(key) + if equal: + self._adjust_for_before(before, i) + else: + self.current_index = i + + def seek_first(self) -> None: + """Seek to the left boundary (i.e. just before the least element). + + A subsequent next() will return the least element if the BTree isn't empty.""" + self.current_node = None + self.current_index = 0 + self.recurse = False + self.increasing = True + self.parents = [] + self.parked = False + self.parking_key = None + + def seek_last(self) -> None: + """Seek to the right boundary (i.e. just after the greatest element). + + A subsequent prev() will return the greatest element if the BTree isn't empty. + """ + self.current_node = None + self.current_index = 1 + self.recurse = False + self.increasing = False + self.parents = [] + self.parked = False + self.parking_key = None + + def __enter__(self): + self.btree.register_cursor(self) + return self + + def __exit__(self, exc_type, exc_value, traceback): + self.btree.deregister_cursor(self) + return False + + +class Immutable(Exception): + """The BTree is immutable.""" + + +class BTree(Generic[KT, ET]): + """An in-memory BTree with copy-on-write and cursors.""" + + def __init__(self, *, t: int = DEFAULT_T, original: Optional["BTree"] = None): + """Create a BTree. + + If *original* is not ``None``, then the BTree is shallow-cloned from + *original* using copy-on-write. Otherwise a new BTree with the specified + *t* value is created. + + The BTree is not thread-safe. + """ + # We don't use a reference to ourselves as a creator as we don't want + # to prevent GC of old btrees. + self.creator = _Creator() + self._immutable = False + self.t: int + self.root: _Node + self.size: int + self.cursors: set[Cursor] = set() + if original is not None: + if not original._immutable: + raise ValueError("original BTree is not immutable") + self.t = original.t + self.root = original.root + self.size = original.size + else: + if t < 3: + raise ValueError("t must be >= 3") + self.t = t + self.root = _Node(self.t, self.creator, True) + self.size = 0 + + def make_immutable(self): + """Make the BTree immutable. + + Attempts to alter the BTree after making it immutable will raise an + Immutable exception. This operation cannot be undone. + """ + if not self._immutable: + self._immutable = True + + def _check_mutable_and_park(self) -> None: + if self._immutable: + raise Immutable + for cursor in self.cursors: + cursor.park() + + # Note that we don't use insert() and delete() but rather insert_element() and + # delete_key() so that BTreeDict can be a proper MutableMapping and supply the + # rest of the standard mapping API. + + def insert_element(self, elt: ET, in_order: bool = False) -> ET | None: + """Insert the element into the BTree. + + If *in_order* is ``True``, then extra work will be done to make left siblings + full, which optimizes storage space when the the elements are inserted in-order + or close to it. + + Returns the previously existing element at the element's key or ``None``. + """ + self._check_mutable_and_park() + cloned = self.root.maybe_cow(self.creator) + if cloned: + self.root = cloned + if self.root.is_maximal(): + old_root = self.root + self.root = _Node(self.t, self.creator, False) + self.root.adopt(*old_root.split()) + oelt = self.root.insert_nonfull(elt, in_order) + if oelt is None: + # We did not replace, so something was added. + self.size += 1 + return oelt + + def get_element(self, key: KT) -> ET | None: + """Get the element matching *key* from the BTree, or return ``None`` if it + does not exist. + """ + return self.root.get(key) + + def _delete(self, key: KT, exact: ET | None) -> ET | None: + self._check_mutable_and_park() + cloned = self.root.maybe_cow(self.creator) + if cloned: + self.root = cloned + elt = self.root.delete(key, None, exact) + if elt is not None: + # We deleted something + self.size -= 1 + if len(self.root.elts) == 0: + # The root is now empty. If there is a child, then collapse this root + # level and make the child the new root. + if not self.root.is_leaf: + assert len(self.root.children) == 1 + self.root = self.root.children[0] + return elt + + def delete_key(self, key: KT) -> ET | None: + """Delete the element matching *key* from the BTree. + + Returns the matching element or ``None`` if it does not exist. + """ + return self._delete(key, None) + + def delete_exact(self, element: ET) -> ET | None: + """Delete *element* from the BTree. + + Returns the matching element or ``None`` if it was not in the BTree. + """ + delt = self._delete(element.key(), element) + assert delt is element + return delt + + def __len__(self): + return self.size + + def visit_in_order(self, visit: Callable[[ET], None]) -> None: + """Call *visit*(element) on all elements in the tree in sorted order.""" + self.root.visit_in_order(visit) + + def _visit_preorder_by_node(self, visit: Callable[[_Node], None]) -> None: + self.root._visit_preorder_by_node(visit) + + def cursor(self) -> Cursor[KT, ET]: + """Create a cursor.""" + return Cursor(self) + + def register_cursor(self, cursor: Cursor) -> None: + """Register a cursor for the automatic parking service.""" + self.cursors.add(cursor) + + def deregister_cursor(self, cursor: Cursor) -> None: + """Deregister a cursor from the automatic parking service.""" + self.cursors.discard(cursor) + + def __copy__(self): + return self.__class__(original=self) + + def __iter__(self): + with self.cursor() as cursor: + while True: + elt = cursor.next() + if elt is None: + break + yield elt.key() + + +VT = TypeVar("VT") # the type of a value in a BTreeDict + + +class KV(Element, Generic[KT, VT]): + """The BTree element type used in a ``BTreeDict``.""" + + def __init__(self, key: KT, value: VT): + self._key = key + self._value = value + + def key(self) -> KT: + return self._key + + def value(self) -> VT: + return self._value + + def __str__(self): # pragma: no cover + return f"KV({self._key}, {self._value})" + + def __repr__(self): # pragma: no cover + return f"KV({self._key}, {self._value})" + + +class BTreeDict(Generic[KT, VT], BTree[KT, KV[KT, VT]], MutableMapping[KT, VT]): + """A MutableMapping implemented with a BTree. + + Unlike a normal Python dict, the BTreeDict may be mutated while iterating. + """ + + def __init__( + self, + *, + t: int = DEFAULT_T, + original: BTree | None = None, + in_order: bool = False, + ): + super().__init__(t=t, original=original) + self.in_order = in_order + + def __getitem__(self, key: KT) -> VT: + elt = self.get_element(key) + if elt is None: + raise KeyError + else: + return cast(KV, elt).value() + + def __setitem__(self, key: KT, value: VT) -> None: + elt = KV(key, value) + self.insert_element(elt, self.in_order) + + def __delitem__(self, key: KT) -> None: + if self.delete_key(key) is None: + raise KeyError + + +class Member(Element, Generic[KT]): + """The BTree element type used in a ``BTreeSet``.""" + + def __init__(self, key: KT): + self._key = key + + def key(self) -> KT: + return self._key + + +class BTreeSet(BTree, Generic[KT], MutableSet[KT]): + """A MutableSet implemented with a BTree. + + Unlike a normal Python set, the BTreeSet may be mutated while iterating. + """ + + def __init__( + self, + *, + t: int = DEFAULT_T, + original: BTree | None = None, + in_order: bool = False, + ): + super().__init__(t=t, original=original) + self.in_order = in_order + + def __contains__(self, key: Any) -> bool: + return self.get_element(key) is not None + + def add(self, value: KT) -> None: + elt = Member(value) + self.insert_element(elt, self.in_order) + + def discard(self, value: KT) -> None: + self.delete_key(value) diff --git a/netdeploy/lib/python3.11/site-packages/dns/btreezone.py b/netdeploy/lib/python3.11/site-packages/dns/btreezone.py new file mode 100644 index 0000000..27b5bb6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/btreezone.py @@ -0,0 +1,367 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# A derivative of a dnspython VersionedZone and related classes, using a BTreeDict and +# a separate per-version delegation index. These additions let us +# +# 1) Do efficient CoW versioning (useful for future online updates). +# 2) Maintain sort order +# 3) Allow delegations to be found easily +# 4) Handle glue +# 5) Add Node flags ORIGIN, DELEGATION, and GLUE whenever relevant. The ORIGIN +# flag is set at the origin node, the DELEGATION FLAG is set at delegation +# points, and the GLUE flag is set on nodes beneath delegation points. + +import enum +from dataclasses import dataclass +from typing import Callable, MutableMapping, Tuple, cast + +import dns.btree +import dns.immutable +import dns.name +import dns.node +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.versioned +import dns.zone + + +class NodeFlags(enum.IntFlag): + ORIGIN = 0x01 + DELEGATION = 0x02 + GLUE = 0x04 + + +class Node(dns.node.Node): + __slots__ = ["flags", "id"] + + def __init__(self, flags: NodeFlags | None = None): + super().__init__() + if flags is None: + # We allow optional flags rather than a default + # as pyright doesn't like assigning a literal 0 + # to flags. + flags = NodeFlags(0) + self.flags = flags + self.id = 0 + + def is_delegation(self): + return (self.flags & NodeFlags.DELEGATION) != 0 + + def is_glue(self): + return (self.flags & NodeFlags.GLUE) != 0 + + def is_origin(self): + return (self.flags & NodeFlags.ORIGIN) != 0 + + def is_origin_or_glue(self): + return (self.flags & (NodeFlags.ORIGIN | NodeFlags.GLUE)) != 0 + + +@dns.immutable.immutable +class ImmutableNode(Node): + def __init__(self, node: Node): + super().__init__() + self.id = node.id + self.rdatasets = tuple( # type: ignore + [dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets] + ) + self.flags = node.flags + + def find_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset: + if create: + raise TypeError("immutable") + return super().find_rdataset(rdclass, rdtype, covers, False) + + def get_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset | None: + if create: + raise TypeError("immutable") + return super().get_rdataset(rdclass, rdtype, covers, False) + + def delete_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + ) -> None: + raise TypeError("immutable") + + def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None: + raise TypeError("immutable") + + def is_immutable(self) -> bool: + return True + + +class Delegations(dns.btree.BTreeSet[dns.name.Name]): + def get_delegation(self, name: dns.name.Name) -> Tuple[dns.name.Name | None, bool]: + """Get the delegation applicable to *name*, if it exists. + + If there delegation, then return a tuple consisting of the name of + the delegation point, and a boolean which is `True` if the name is a proper + subdomain of the delegation point, and `False` if it is equal to the delegation + point. + """ + cursor = self.cursor() + cursor.seek(name, before=False) + prev = cursor.prev() + if prev is None: + return None, False + cut = prev.key() + reln, _, _ = name.fullcompare(cut) + is_subdomain = reln == dns.name.NameRelation.SUBDOMAIN + if is_subdomain or reln == dns.name.NameRelation.EQUAL: + return cut, is_subdomain + else: + return None, False + + def is_glue(self, name: dns.name.Name) -> bool: + """Is *name* glue, i.e. is it beneath a delegation?""" + cursor = self.cursor() + cursor.seek(name, before=False) + cut, is_subdomain = self.get_delegation(name) + if cut is None: + return False + return is_subdomain + + +class WritableVersion(dns.zone.WritableVersion): + def __init__(self, zone: dns.zone.Zone, replacement: bool = False): + super().__init__(zone, True) + if not replacement: + assert isinstance(zone, dns.versioned.Zone) + version = zone._versions[-1] + self.nodes: dns.btree.BTreeDict[dns.name.Name, Node] = dns.btree.BTreeDict( + original=version.nodes # type: ignore + ) + self.delegations = Delegations(original=version.delegations) # type: ignore + else: + self.delegations = Delegations() + + def _is_origin(self, name: dns.name.Name) -> bool: + # Assumes name has already been validated (and thus adjusted to the right + # relativity too) + if self.zone.relativize: + return name == dns.name.empty + else: + return name == self.zone.origin + + def _maybe_cow_with_name( + self, name: dns.name.Name + ) -> Tuple[dns.node.Node, dns.name.Name]: + (node, name) = super()._maybe_cow_with_name(name) + node = cast(Node, node) + if self._is_origin(name): + node.flags |= NodeFlags.ORIGIN + elif self.delegations.is_glue(name): + node.flags |= NodeFlags.GLUE + return (node, name) + + def update_glue_flag(self, name: dns.name.Name, is_glue: bool) -> None: + cursor = self.nodes.cursor() # type: ignore + cursor.seek(name, False) + updates = [] + while True: + elt = cursor.next() + if elt is None: + break + ename = elt.key() + if not ename.is_subdomain(name): + break + node = cast(dns.node.Node, elt.value()) + if ename not in self.changed: + new_node = self.zone.node_factory() + new_node.id = self.id # type: ignore + new_node.rdatasets.extend(node.rdatasets) + self.changed.add(ename) + node = new_node + assert isinstance(node, Node) + if is_glue: + node.flags |= NodeFlags.GLUE + else: + node.flags &= ~NodeFlags.GLUE + # We don't update node here as any insertion could disturb the + # btree and invalidate our cursor. We could use the cursor in a + # with block and avoid this, but it would do a lot of parking and + # unparking so the deferred update mode may still be better. + updates.append((ename, node)) + for ename, node in updates: + self.nodes[ename] = node + + def delete_node(self, name: dns.name.Name) -> None: + name = self._validate_name(name) + node = self.nodes.get(name) + if node is not None: + if node.is_delegation(): # type: ignore + self.delegations.discard(name) + self.update_glue_flag(name, False) + del self.nodes[name] + self.changed.add(name) + + def put_rdataset( + self, name: dns.name.Name, rdataset: dns.rdataset.Rdataset + ) -> None: + (node, name) = self._maybe_cow_with_name(name) + if ( + rdataset.rdtype == dns.rdatatype.NS and not node.is_origin_or_glue() # type: ignore + ): + node.flags |= NodeFlags.DELEGATION # type: ignore + if name not in self.delegations: + self.delegations.add(name) + self.update_glue_flag(name, True) + node.replace_rdataset(rdataset) + + def delete_rdataset( + self, + name: dns.name.Name, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType, + ) -> None: + (node, name) = self._maybe_cow_with_name(name) + if rdtype == dns.rdatatype.NS and name in self.delegations: # type: ignore + node.flags &= ~NodeFlags.DELEGATION # type: ignore + self.delegations.discard(name) # type: ignore + self.update_glue_flag(name, False) + node.delete_rdataset(self.zone.rdclass, rdtype, covers) + if len(node) == 0: + del self.nodes[name] + + +@dataclass(frozen=True) +class Bounds: + name: dns.name.Name + left: dns.name.Name + right: dns.name.Name | None + closest_encloser: dns.name.Name + is_equal: bool + is_delegation: bool + + def __str__(self): + if self.is_equal: + op = "=" + else: + op = "<" + if self.is_delegation: + zonecut = " zonecut" + else: + zonecut = "" + return ( + f"{self.left} {op} {self.name} < {self.right}{zonecut}; " + f"{self.closest_encloser}" + ) + + +@dns.immutable.immutable +class ImmutableVersion(dns.zone.Version): + def __init__(self, version: dns.zone.Version): + if not isinstance(version, WritableVersion): + raise ValueError( + "a dns.btreezone.ImmutableVersion requires a " + "dns.btreezone.WritableVersion" + ) + super().__init__(version.zone, True) + self.id = version.id + self.origin = version.origin + for name in version.changed: + node = version.nodes.get(name) + if node: + version.nodes[name] = ImmutableNode(node) + # the cast below is for mypy + self.nodes = cast(MutableMapping[dns.name.Name, dns.node.Node], version.nodes) + self.nodes.make_immutable() # type: ignore + self.delegations = version.delegations + self.delegations.make_immutable() + + def bounds(self, name: dns.name.Name | str) -> Bounds: + """Return the 'bounds' of *name* in its zone. + + The bounds information is useful when making an authoritative response, as + it can be used to determine whether the query name is at or beneath a delegation + point. The other data in the ``Bounds`` object is useful for making on-the-fly + DNSSEC signatures. + + The left bound of *name* is *name* itself if it is in the zone, or the greatest + predecessor which is in the zone. + + The right bound of *name* is the least successor of *name*, or ``None`` if + no name in the zone is greater than *name*. + + The closest encloser of *name* is *name* itself, if *name* is in the zone; + otherwise it is the name with the largest number of labels in common with + *name* that is in the zone, either explicitly or by the implied existence + of empty non-terminals. + + The bounds *is_equal* field is ``True`` if and only if *name* is equal to + its left bound. + + The bounds *is_delegation* field is ``True`` if and only if the left bound is a + delegation point. + """ + assert self.origin is not None + # validate the origin because we may need to relativize + origin = self.zone._validate_name(self.origin) + name = self.zone._validate_name(name) + cut, _ = self.delegations.get_delegation(name) + if cut is not None: + target = cut + is_delegation = True + else: + target = name + is_delegation = False + c = cast(dns.btree.BTreeDict, self.nodes).cursor() + c.seek(target, False) + left = c.prev() + assert left is not None + c.next() # skip over left + while True: + right = c.next() + if right is None or not right.value().is_glue(): + break + left_comparison = left.key().fullcompare(name) + if right is not None: + right_key = right.key() + right_comparison = right_key.fullcompare(name) + else: + right_comparison = ( + dns.name.NAMERELN_COMMONANCESTOR, + -1, + len(origin), + ) + right_key = None + closest_encloser = dns.name.Name( + name[-max(left_comparison[2], right_comparison[2]) :] + ) + return Bounds( + name, + left.key(), + right_key, + closest_encloser, + left_comparison[0] == dns.name.NameRelation.EQUAL, + is_delegation, + ) + + +class Zone(dns.versioned.Zone): + node_factory: Callable[[], dns.node.Node] = Node + map_factory: Callable[[], MutableMapping[dns.name.Name, dns.node.Node]] = cast( + Callable[[], MutableMapping[dns.name.Name, dns.node.Node]], + dns.btree.BTreeDict[dns.name.Name, Node], + ) + writable_version_factory: ( + Callable[[dns.zone.Zone, bool], dns.zone.Version] | None + ) = WritableVersion + immutable_version_factory: Callable[[dns.zone.Version], dns.zone.Version] | None = ( + ImmutableVersion + ) diff --git a/netdeploy/lib/python3.11/site-packages/dns/dnssec.py b/netdeploy/lib/python3.11/site-packages/dns/dnssec.py new file mode 100644 index 0000000..0b2aa70 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/dnssec.py @@ -0,0 +1,1242 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Common DNSSEC-related functions and constants.""" + +# pylint: disable=unused-import + +import base64 +import contextlib +import functools +import hashlib +import struct +import time +from datetime import datetime +from typing import Callable, Dict, List, Set, Tuple, Union, cast + +import dns._features +import dns.name +import dns.node +import dns.rdata +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.rrset +import dns.transaction +import dns.zone +from dns.dnssectypes import Algorithm, DSDigest, NSEC3Hash +from dns.exception import AlgorithmKeyMismatch as AlgorithmKeyMismatch +from dns.exception import DeniedByPolicy, UnsupportedAlgorithm, ValidationFailure +from dns.rdtypes.ANY.CDNSKEY import CDNSKEY +from dns.rdtypes.ANY.CDS import CDS +from dns.rdtypes.ANY.DNSKEY import DNSKEY +from dns.rdtypes.ANY.DS import DS +from dns.rdtypes.ANY.NSEC import NSEC, Bitmap +from dns.rdtypes.ANY.NSEC3PARAM import NSEC3PARAM +from dns.rdtypes.ANY.RRSIG import RRSIG, sigtime_to_posixtime +from dns.rdtypes.dnskeybase import Flag + +PublicKey = Union[ + "GenericPublicKey", + "rsa.RSAPublicKey", + "ec.EllipticCurvePublicKey", + "ed25519.Ed25519PublicKey", + "ed448.Ed448PublicKey", +] + +PrivateKey = Union[ + "GenericPrivateKey", + "rsa.RSAPrivateKey", + "ec.EllipticCurvePrivateKey", + "ed25519.Ed25519PrivateKey", + "ed448.Ed448PrivateKey", +] + +RRsetSigner = Callable[[dns.transaction.Transaction, dns.rrset.RRset], None] + + +def algorithm_from_text(text: str) -> Algorithm: + """Convert text into a DNSSEC algorithm value. + + *text*, a ``str``, the text to convert to into an algorithm value. + + Returns an ``int``. + """ + + return Algorithm.from_text(text) + + +def algorithm_to_text(value: Algorithm | int) -> str: + """Convert a DNSSEC algorithm value to text + + *value*, a ``dns.dnssec.Algorithm``. + + Returns a ``str``, the name of a DNSSEC algorithm. + """ + + return Algorithm.to_text(value) + + +def to_timestamp(value: datetime | str | float | int) -> int: + """Convert various format to a timestamp""" + if isinstance(value, datetime): + return int(value.timestamp()) + elif isinstance(value, str): + return sigtime_to_posixtime(value) + elif isinstance(value, float): + return int(value) + elif isinstance(value, int): + return value + else: + raise TypeError("Unsupported timestamp type") + + +def key_id(key: DNSKEY | CDNSKEY) -> int: + """Return the key id (a 16-bit number) for the specified key. + + *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY`` + + Returns an ``int`` between 0 and 65535 + """ + + rdata = key.to_wire() + assert rdata is not None # for mypy + if key.algorithm == Algorithm.RSAMD5: + return (rdata[-3] << 8) + rdata[-2] + else: + total = 0 + for i in range(len(rdata) // 2): + total += (rdata[2 * i] << 8) + rdata[2 * i + 1] + if len(rdata) % 2 != 0: + total += rdata[len(rdata) - 1] << 8 + total += (total >> 16) & 0xFFFF + return total & 0xFFFF + + +class Policy: + def __init__(self): + pass + + def ok_to_sign(self, key: DNSKEY) -> bool: # pragma: no cover + return False + + def ok_to_validate(self, key: DNSKEY) -> bool: # pragma: no cover + return False + + def ok_to_create_ds(self, algorithm: DSDigest) -> bool: # pragma: no cover + return False + + def ok_to_validate_ds(self, algorithm: DSDigest) -> bool: # pragma: no cover + return False + + +class SimpleDeny(Policy): + def __init__(self, deny_sign, deny_validate, deny_create_ds, deny_validate_ds): + super().__init__() + self._deny_sign = deny_sign + self._deny_validate = deny_validate + self._deny_create_ds = deny_create_ds + self._deny_validate_ds = deny_validate_ds + + def ok_to_sign(self, key: DNSKEY) -> bool: + return key.algorithm not in self._deny_sign + + def ok_to_validate(self, key: DNSKEY) -> bool: + return key.algorithm not in self._deny_validate + + def ok_to_create_ds(self, algorithm: DSDigest) -> bool: + return algorithm not in self._deny_create_ds + + def ok_to_validate_ds(self, algorithm: DSDigest) -> bool: + return algorithm not in self._deny_validate_ds + + +rfc_8624_policy = SimpleDeny( + {Algorithm.RSAMD5, Algorithm.DSA, Algorithm.DSANSEC3SHA1, Algorithm.ECCGOST}, + {Algorithm.RSAMD5, Algorithm.DSA, Algorithm.DSANSEC3SHA1}, + {DSDigest.NULL, DSDigest.SHA1, DSDigest.GOST}, + {DSDigest.NULL}, +) + +allow_all_policy = SimpleDeny(set(), set(), set(), set()) + + +default_policy = rfc_8624_policy + + +def make_ds( + name: dns.name.Name | str, + key: dns.rdata.Rdata, + algorithm: DSDigest | str, + origin: dns.name.Name | None = None, + policy: Policy | None = None, + validating: bool = False, +) -> DS: + """Create a DS record for a DNSSEC key. + + *name*, a ``dns.name.Name`` or ``str``, the owner name of the DS record. + + *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY`` or ``dns.rdtypes.ANY.DNSKEY.CDNSKEY``, + the key the DS is about. + + *algorithm*, a ``str`` or ``int`` specifying the hash algorithm. + The currently supported hashes are "SHA1", "SHA256", and "SHA384". Case + does not matter for these strings. + + *origin*, a ``dns.name.Name`` or ``None``. If *key* is a relative name, + then it will be made absolute using the specified origin. + + *policy*, a ``dns.dnssec.Policy`` or ``None``. If ``None``, the default policy, + ``dns.dnssec.default_policy`` is used; this policy defaults to that of RFC 8624. + + *validating*, a ``bool``. If ``True``, then policy is checked in + validating mode, i.e. "Is it ok to validate using this digest algorithm?". + Otherwise the policy is checked in creating mode, i.e. "Is it ok to create a DS with + this digest algorithm?". + + Raises ``UnsupportedAlgorithm`` if the algorithm is unknown. + + Raises ``DeniedByPolicy`` if the algorithm is denied by policy. + + Returns a ``dns.rdtypes.ANY.DS.DS`` + """ + + if policy is None: + policy = default_policy + try: + if isinstance(algorithm, str): + algorithm = DSDigest[algorithm.upper()] + except Exception: + raise UnsupportedAlgorithm(f'unsupported algorithm "{algorithm}"') + if validating: + check = policy.ok_to_validate_ds + else: + check = policy.ok_to_create_ds + if not check(algorithm): + raise DeniedByPolicy + if not isinstance(key, DNSKEY | CDNSKEY): + raise ValueError("key is not a DNSKEY | CDNSKEY") + if algorithm == DSDigest.SHA1: + dshash = hashlib.sha1() + elif algorithm == DSDigest.SHA256: + dshash = hashlib.sha256() + elif algorithm == DSDigest.SHA384: + dshash = hashlib.sha384() + else: + raise UnsupportedAlgorithm(f'unsupported algorithm "{algorithm}"') + + if isinstance(name, str): + name = dns.name.from_text(name, origin) + wire = name.canonicalize().to_wire() + kwire = key.to_wire(origin=origin) + assert wire is not None and kwire is not None # for mypy + dshash.update(wire) + dshash.update(kwire) + digest = dshash.digest() + + dsrdata = struct.pack("!HBB", key_id(key), key.algorithm, algorithm) + digest + ds = dns.rdata.from_wire( + dns.rdataclass.IN, dns.rdatatype.DS, dsrdata, 0, len(dsrdata) + ) + return cast(DS, ds) + + +def make_cds( + name: dns.name.Name | str, + key: dns.rdata.Rdata, + algorithm: DSDigest | str, + origin: dns.name.Name | None = None, +) -> CDS: + """Create a CDS record for a DNSSEC key. + + *name*, a ``dns.name.Name`` or ``str``, the owner name of the DS record. + + *key*, a ``dns.rdtypes.ANY.DNSKEY.DNSKEY`` or ``dns.rdtypes.ANY.DNSKEY.CDNSKEY``, + the key the DS is about. + + *algorithm*, a ``str`` or ``int`` specifying the hash algorithm. + The currently supported hashes are "SHA1", "SHA256", and "SHA384". Case + does not matter for these strings. + + *origin*, a ``dns.name.Name`` or ``None``. If *key* is a relative name, + then it will be made absolute using the specified origin. + + Raises ``UnsupportedAlgorithm`` if the algorithm is unknown. + + Returns a ``dns.rdtypes.ANY.DS.CDS`` + """ + + ds = make_ds(name, key, algorithm, origin) + return CDS( + rdclass=ds.rdclass, + rdtype=dns.rdatatype.CDS, + key_tag=ds.key_tag, + algorithm=ds.algorithm, + digest_type=ds.digest_type, + digest=ds.digest, + ) + + +def _find_candidate_keys( + keys: Dict[dns.name.Name, dns.rdataset.Rdataset | dns.node.Node], rrsig: RRSIG +) -> List[DNSKEY] | None: + value = keys.get(rrsig.signer) + if isinstance(value, dns.node.Node): + rdataset = value.get_rdataset(dns.rdataclass.IN, dns.rdatatype.DNSKEY) + else: + rdataset = value + if rdataset is None: + return None + return [ + cast(DNSKEY, rd) + for rd in rdataset + if rd.algorithm == rrsig.algorithm + and key_id(rd) == rrsig.key_tag + and (rd.flags & Flag.ZONE) == Flag.ZONE # RFC 4034 2.1.1 + and rd.protocol == 3 # RFC 4034 2.1.2 + ] + + +def _get_rrname_rdataset( + rrset: dns.rrset.RRset | Tuple[dns.name.Name, dns.rdataset.Rdataset], +) -> Tuple[dns.name.Name, dns.rdataset.Rdataset]: + if isinstance(rrset, tuple): + return rrset[0], rrset[1] + else: + return rrset.name, rrset + + +def _validate_signature(sig: bytes, data: bytes, key: DNSKEY) -> None: + # pylint: disable=possibly-used-before-assignment + public_cls = get_algorithm_cls_from_dnskey(key).public_cls + try: + public_key = public_cls.from_dnskey(key) + except ValueError: + raise ValidationFailure("invalid public key") + public_key.verify(sig, data) + + +def _validate_rrsig( + rrset: dns.rrset.RRset | Tuple[dns.name.Name, dns.rdataset.Rdataset], + rrsig: RRSIG, + keys: Dict[dns.name.Name, dns.node.Node | dns.rdataset.Rdataset], + origin: dns.name.Name | None = None, + now: float | None = None, + policy: Policy | None = None, +) -> None: + """Validate an RRset against a single signature rdata, throwing an + exception if validation is not successful. + + *rrset*, the RRset to validate. This can be a + ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``) + tuple. + + *rrsig*, a ``dns.rdata.Rdata``, the signature to validate. + + *keys*, the key dictionary, used to find the DNSKEY associated + with a given name. The dictionary is keyed by a + ``dns.name.Name``, and has ``dns.node.Node`` or + ``dns.rdataset.Rdataset`` values. + + *origin*, a ``dns.name.Name`` or ``None``, the origin to use for relative + names. + + *now*, a ``float`` or ``None``, the time, in seconds since the epoch, to + use as the current time when validating. If ``None``, the actual current + time is used. + + *policy*, a ``dns.dnssec.Policy`` or ``None``. If ``None``, the default policy, + ``dns.dnssec.default_policy`` is used; this policy defaults to that of RFC 8624. + + Raises ``ValidationFailure`` if the signature is expired, not yet valid, + the public key is invalid, the algorithm is unknown, the verification + fails, etc. + + Raises ``UnsupportedAlgorithm`` if the algorithm is recognized by + dnspython but not implemented. + """ + + if policy is None: + policy = default_policy + + candidate_keys = _find_candidate_keys(keys, rrsig) + if candidate_keys is None: + raise ValidationFailure("unknown key") + + if now is None: + now = time.time() + if rrsig.expiration < now: + raise ValidationFailure("expired") + if rrsig.inception > now: + raise ValidationFailure("not yet valid") + + data = _make_rrsig_signature_data(rrset, rrsig, origin) + + # pylint: disable=possibly-used-before-assignment + for candidate_key in candidate_keys: + if not policy.ok_to_validate(candidate_key): + continue + try: + _validate_signature(rrsig.signature, data, candidate_key) + return + except (InvalidSignature, ValidationFailure): + # this happens on an individual validation failure + continue + # nothing verified -- raise failure: + raise ValidationFailure("verify failure") + + +def _validate( + rrset: dns.rrset.RRset | Tuple[dns.name.Name, dns.rdataset.Rdataset], + rrsigset: dns.rrset.RRset | Tuple[dns.name.Name, dns.rdataset.Rdataset], + keys: Dict[dns.name.Name, dns.node.Node | dns.rdataset.Rdataset], + origin: dns.name.Name | None = None, + now: float | None = None, + policy: Policy | None = None, +) -> None: + """Validate an RRset against a signature RRset, throwing an exception + if none of the signatures validate. + + *rrset*, the RRset to validate. This can be a + ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``) + tuple. + + *rrsigset*, the signature RRset. This can be a + ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``) + tuple. + + *keys*, the key dictionary, used to find the DNSKEY associated + with a given name. The dictionary is keyed by a + ``dns.name.Name``, and has ``dns.node.Node`` or + ``dns.rdataset.Rdataset`` values. + + *origin*, a ``dns.name.Name``, the origin to use for relative names; + defaults to None. + + *now*, an ``int`` or ``None``, the time, in seconds since the epoch, to + use as the current time when validating. If ``None``, the actual current + time is used. + + *policy*, a ``dns.dnssec.Policy`` or ``None``. If ``None``, the default policy, + ``dns.dnssec.default_policy`` is used; this policy defaults to that of RFC 8624. + + Raises ``ValidationFailure`` if the signature is expired, not yet valid, + the public key is invalid, the algorithm is unknown, the verification + fails, etc. + """ + + if policy is None: + policy = default_policy + + if isinstance(origin, str): + origin = dns.name.from_text(origin, dns.name.root) + + if isinstance(rrset, tuple): + rrname = rrset[0] + else: + rrname = rrset.name + + if isinstance(rrsigset, tuple): + rrsigname = rrsigset[0] + rrsigrdataset = rrsigset[1] + else: + rrsigname = rrsigset.name + rrsigrdataset = rrsigset + + rrname = rrname.choose_relativity(origin) + rrsigname = rrsigname.choose_relativity(origin) + if rrname != rrsigname: + raise ValidationFailure("owner names do not match") + + for rrsig in rrsigrdataset: + if not isinstance(rrsig, RRSIG): + raise ValidationFailure("expected an RRSIG") + try: + _validate_rrsig(rrset, rrsig, keys, origin, now, policy) + return + except (ValidationFailure, UnsupportedAlgorithm): + pass + raise ValidationFailure("no RRSIGs validated") + + +def _sign( + rrset: dns.rrset.RRset | Tuple[dns.name.Name, dns.rdataset.Rdataset], + private_key: PrivateKey, + signer: dns.name.Name, + dnskey: DNSKEY, + inception: datetime | str | int | float | None = None, + expiration: datetime | str | int | float | None = None, + lifetime: int | None = None, + verify: bool = False, + policy: Policy | None = None, + origin: dns.name.Name | None = None, + deterministic: bool = True, +) -> RRSIG: + """Sign RRset using private key. + + *rrset*, the RRset to validate. This can be a + ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``) + tuple. + + *private_key*, the private key to use for signing, a + ``cryptography.hazmat.primitives.asymmetric`` private key class applicable + for DNSSEC. + + *signer*, a ``dns.name.Name``, the Signer's name. + + *dnskey*, a ``DNSKEY`` matching ``private_key``. + + *inception*, a ``datetime``, ``str``, ``int``, ``float`` or ``None``, the + signature inception time. If ``None``, the current time is used. If a ``str``, the + format is "YYYYMMDDHHMMSS" or alternatively the number of seconds since the UNIX + epoch in text form; this is the same the RRSIG rdata's text form. + Values of type `int` or `float` are interpreted as seconds since the UNIX epoch. + + *expiration*, a ``datetime``, ``str``, ``int``, ``float`` or ``None``, the signature + expiration time. If ``None``, the expiration time will be the inception time plus + the value of the *lifetime* parameter. See the description of *inception* above + for how the various parameter types are interpreted. + + *lifetime*, an ``int`` or ``None``, the signature lifetime in seconds. This + parameter is only meaningful if *expiration* is ``None``. + + *verify*, a ``bool``. If set to ``True``, the signer will verify signatures + after they are created; the default is ``False``. + + *policy*, a ``dns.dnssec.Policy`` or ``None``. If ``None``, the default policy, + ``dns.dnssec.default_policy`` is used; this policy defaults to that of RFC 8624. + + *origin*, a ``dns.name.Name`` or ``None``. If ``None``, the default, then all + names in the rrset (including its owner name) must be absolute; otherwise the + specified origin will be used to make names absolute when signing. + + *deterministic*, a ``bool``. If ``True``, the default, use deterministic + (reproducible) signatures when supported by the algorithm used for signing. + Currently, this only affects ECDSA. + + Raises ``DeniedByPolicy`` if the signature is denied by policy. + """ + + if policy is None: + policy = default_policy + if not policy.ok_to_sign(dnskey): + raise DeniedByPolicy + + if isinstance(rrset, tuple): + rdclass = rrset[1].rdclass + rdtype = rrset[1].rdtype + rrname = rrset[0] + original_ttl = rrset[1].ttl + else: + rdclass = rrset.rdclass + rdtype = rrset.rdtype + rrname = rrset.name + original_ttl = rrset.ttl + + if inception is not None: + rrsig_inception = to_timestamp(inception) + else: + rrsig_inception = int(time.time()) + + if expiration is not None: + rrsig_expiration = to_timestamp(expiration) + elif lifetime is not None: + rrsig_expiration = rrsig_inception + lifetime + else: + raise ValueError("expiration or lifetime must be specified") + + # Derelativize now because we need a correct labels length for the + # rrsig_template. + if origin is not None: + rrname = rrname.derelativize(origin) + labels = len(rrname) - 1 + + # Adjust labels appropriately for wildcards. + if rrname.is_wild(): + labels -= 1 + + rrsig_template = RRSIG( + rdclass=rdclass, + rdtype=dns.rdatatype.RRSIG, + type_covered=rdtype, + algorithm=dnskey.algorithm, + labels=labels, + original_ttl=original_ttl, + expiration=rrsig_expiration, + inception=rrsig_inception, + key_tag=key_id(dnskey), + signer=signer, + signature=b"", + ) + + data = _make_rrsig_signature_data(rrset, rrsig_template, origin) + + # pylint: disable=possibly-used-before-assignment + if isinstance(private_key, GenericPrivateKey): + signing_key = private_key + else: + try: + private_cls = get_algorithm_cls_from_dnskey(dnskey) + signing_key = private_cls(key=private_key) + except UnsupportedAlgorithm: + raise TypeError("Unsupported key algorithm") + + signature = signing_key.sign(data, verify, deterministic) + + return cast(RRSIG, rrsig_template.replace(signature=signature)) + + +def _make_rrsig_signature_data( + rrset: dns.rrset.RRset | Tuple[dns.name.Name, dns.rdataset.Rdataset], + rrsig: RRSIG, + origin: dns.name.Name | None = None, +) -> bytes: + """Create signature rdata. + + *rrset*, the RRset to sign/validate. This can be a + ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``) + tuple. + + *rrsig*, a ``dns.rdata.Rdata``, the signature to validate, or the + signature template used when signing. + + *origin*, a ``dns.name.Name`` or ``None``, the origin to use for relative + names. + + Raises ``UnsupportedAlgorithm`` if the algorithm is recognized by + dnspython but not implemented. + """ + + if isinstance(origin, str): + origin = dns.name.from_text(origin, dns.name.root) + + signer = rrsig.signer + if not signer.is_absolute(): + if origin is None: + raise ValidationFailure("relative RR name without an origin specified") + signer = signer.derelativize(origin) + + # For convenience, allow the rrset to be specified as a (name, + # rdataset) tuple as well as a proper rrset + rrname, rdataset = _get_rrname_rdataset(rrset) + + data = b"" + wire = rrsig.to_wire(origin=signer) + assert wire is not None # for mypy + data += wire[:18] + data += rrsig.signer.to_digestable(signer) + + # Derelativize the name before considering labels. + if not rrname.is_absolute(): + if origin is None: + raise ValidationFailure("relative RR name without an origin specified") + rrname = rrname.derelativize(origin) + + name_len = len(rrname) + if rrname.is_wild() and rrsig.labels != name_len - 2: + raise ValidationFailure("wild owner name has wrong label length") + if name_len - 1 < rrsig.labels: + raise ValidationFailure("owner name longer than RRSIG labels") + elif rrsig.labels < name_len - 1: + suffix = rrname.split(rrsig.labels + 1)[1] + rrname = dns.name.from_text("*", suffix) + rrnamebuf = rrname.to_digestable() + rrfixed = struct.pack("!HHI", rdataset.rdtype, rdataset.rdclass, rrsig.original_ttl) + rdatas = [rdata.to_digestable(origin) for rdata in rdataset] + for rdata in sorted(rdatas): + data += rrnamebuf + data += rrfixed + rrlen = struct.pack("!H", len(rdata)) + data += rrlen + data += rdata + + return data + + +def _make_dnskey( + public_key: PublicKey, + algorithm: int | str, + flags: int = Flag.ZONE, + protocol: int = 3, +) -> DNSKEY: + """Convert a public key to DNSKEY Rdata + + *public_key*, a ``PublicKey`` (``GenericPublicKey`` or + ``cryptography.hazmat.primitives.asymmetric``) to convert. + + *algorithm*, a ``str`` or ``int`` specifying the DNSKEY algorithm. + + *flags*: DNSKEY flags field as an integer. + + *protocol*: DNSKEY protocol field as an integer. + + Raises ``ValueError`` if the specified key algorithm parameters are not + unsupported, ``TypeError`` if the key type is unsupported, + `UnsupportedAlgorithm` if the algorithm is unknown and + `AlgorithmKeyMismatch` if the algorithm does not match the key type. + + Return DNSKEY ``Rdata``. + """ + + algorithm = Algorithm.make(algorithm) + + # pylint: disable=possibly-used-before-assignment + if isinstance(public_key, GenericPublicKey): + return public_key.to_dnskey(flags=flags, protocol=protocol) + else: + public_cls = get_algorithm_cls(algorithm).public_cls + return public_cls(key=public_key).to_dnskey(flags=flags, protocol=protocol) + + +def _make_cdnskey( + public_key: PublicKey, + algorithm: int | str, + flags: int = Flag.ZONE, + protocol: int = 3, +) -> CDNSKEY: + """Convert a public key to CDNSKEY Rdata + + *public_key*, the public key to convert, a + ``cryptography.hazmat.primitives.asymmetric`` public key class applicable + for DNSSEC. + + *algorithm*, a ``str`` or ``int`` specifying the DNSKEY algorithm. + + *flags*: DNSKEY flags field as an integer. + + *protocol*: DNSKEY protocol field as an integer. + + Raises ``ValueError`` if the specified key algorithm parameters are not + unsupported, ``TypeError`` if the key type is unsupported, + `UnsupportedAlgorithm` if the algorithm is unknown and + `AlgorithmKeyMismatch` if the algorithm does not match the key type. + + Return CDNSKEY ``Rdata``. + """ + + dnskey = _make_dnskey(public_key, algorithm, flags, protocol) + + return CDNSKEY( + rdclass=dnskey.rdclass, + rdtype=dns.rdatatype.CDNSKEY, + flags=dnskey.flags, + protocol=dnskey.protocol, + algorithm=dnskey.algorithm, + key=dnskey.key, + ) + + +def nsec3_hash( + domain: dns.name.Name | str, + salt: str | bytes | None, + iterations: int, + algorithm: int | str, +) -> str: + """ + Calculate the NSEC3 hash, according to + https://tools.ietf.org/html/rfc5155#section-5 + + *domain*, a ``dns.name.Name`` or ``str``, the name to hash. + + *salt*, a ``str``, ``bytes``, or ``None``, the hash salt. If a + string, it is decoded as a hex string. + + *iterations*, an ``int``, the number of iterations. + + *algorithm*, a ``str`` or ``int``, the hash algorithm. + The only defined algorithm is SHA1. + + Returns a ``str``, the encoded NSEC3 hash. + """ + + b32_conversion = str.maketrans( + "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", "0123456789ABCDEFGHIJKLMNOPQRSTUV" + ) + + try: + if isinstance(algorithm, str): + algorithm = NSEC3Hash[algorithm.upper()] + except Exception: + raise ValueError("Wrong hash algorithm (only SHA1 is supported)") + + if algorithm != NSEC3Hash.SHA1: + raise ValueError("Wrong hash algorithm (only SHA1 is supported)") + + if salt is None: + salt_encoded = b"" + elif isinstance(salt, str): + if len(salt) % 2 == 0: + salt_encoded = bytes.fromhex(salt) + else: + raise ValueError("Invalid salt length") + else: + salt_encoded = salt + + if not isinstance(domain, dns.name.Name): + domain = dns.name.from_text(domain) + domain_encoded = domain.canonicalize().to_wire() + assert domain_encoded is not None + + digest = hashlib.sha1(domain_encoded + salt_encoded).digest() + for _ in range(iterations): + digest = hashlib.sha1(digest + salt_encoded).digest() + + output = base64.b32encode(digest).decode("utf-8") + output = output.translate(b32_conversion) + + return output + + +def make_ds_rdataset( + rrset: dns.rrset.RRset | Tuple[dns.name.Name, dns.rdataset.Rdataset], + algorithms: Set[DSDigest | str], + origin: dns.name.Name | None = None, +) -> dns.rdataset.Rdataset: + """Create a DS record from DNSKEY/CDNSKEY/CDS. + + *rrset*, the RRset to create DS Rdataset for. This can be a + ``dns.rrset.RRset`` or a (``dns.name.Name``, ``dns.rdataset.Rdataset``) + tuple. + + *algorithms*, a set of ``str`` or ``int`` specifying the hash algorithms. + The currently supported hashes are "SHA1", "SHA256", and "SHA384". Case + does not matter for these strings. If the RRset is a CDS, only digest + algorithms matching algorithms are accepted. + + *origin*, a ``dns.name.Name`` or ``None``. If `key` is a relative name, + then it will be made absolute using the specified origin. + + Raises ``UnsupportedAlgorithm`` if any of the algorithms are unknown and + ``ValueError`` if the given RRset is not usable. + + Returns a ``dns.rdataset.Rdataset`` + """ + + rrname, rdataset = _get_rrname_rdataset(rrset) + + if rdataset.rdtype not in ( + dns.rdatatype.DNSKEY, + dns.rdatatype.CDNSKEY, + dns.rdatatype.CDS, + ): + raise ValueError("rrset not a DNSKEY/CDNSKEY/CDS") + + _algorithms = set() + for algorithm in algorithms: + try: + if isinstance(algorithm, str): + algorithm = DSDigest[algorithm.upper()] + except Exception: + raise UnsupportedAlgorithm(f'unsupported algorithm "{algorithm}"') + _algorithms.add(algorithm) + + if rdataset.rdtype == dns.rdatatype.CDS: + res = [] + for rdata in cds_rdataset_to_ds_rdataset(rdataset): + if rdata.digest_type in _algorithms: + res.append(rdata) + if len(res) == 0: + raise ValueError("no acceptable CDS rdata found") + return dns.rdataset.from_rdata_list(rdataset.ttl, res) + + res = [] + for algorithm in _algorithms: + res.extend(dnskey_rdataset_to_cds_rdataset(rrname, rdataset, algorithm, origin)) + return dns.rdataset.from_rdata_list(rdataset.ttl, res) + + +def cds_rdataset_to_ds_rdataset( + rdataset: dns.rdataset.Rdataset, +) -> dns.rdataset.Rdataset: + """Create a CDS record from DS. + + *rdataset*, a ``dns.rdataset.Rdataset``, to create DS Rdataset for. + + Raises ``ValueError`` if the rdataset is not CDS. + + Returns a ``dns.rdataset.Rdataset`` + """ + + if rdataset.rdtype != dns.rdatatype.CDS: + raise ValueError("rdataset not a CDS") + res = [] + for rdata in rdataset: + res.append( + CDS( + rdclass=rdata.rdclass, + rdtype=dns.rdatatype.DS, + key_tag=rdata.key_tag, + algorithm=rdata.algorithm, + digest_type=rdata.digest_type, + digest=rdata.digest, + ) + ) + return dns.rdataset.from_rdata_list(rdataset.ttl, res) + + +def dnskey_rdataset_to_cds_rdataset( + name: dns.name.Name | str, + rdataset: dns.rdataset.Rdataset, + algorithm: DSDigest | str, + origin: dns.name.Name | None = None, +) -> dns.rdataset.Rdataset: + """Create a CDS record from DNSKEY/CDNSKEY. + + *name*, a ``dns.name.Name`` or ``str``, the owner name of the CDS record. + + *rdataset*, a ``dns.rdataset.Rdataset``, to create DS Rdataset for. + + *algorithm*, a ``str`` or ``int`` specifying the hash algorithm. + The currently supported hashes are "SHA1", "SHA256", and "SHA384". Case + does not matter for these strings. + + *origin*, a ``dns.name.Name`` or ``None``. If `key` is a relative name, + then it will be made absolute using the specified origin. + + Raises ``UnsupportedAlgorithm`` if the algorithm is unknown or + ``ValueError`` if the rdataset is not DNSKEY/CDNSKEY. + + Returns a ``dns.rdataset.Rdataset`` + """ + + if rdataset.rdtype not in (dns.rdatatype.DNSKEY, dns.rdatatype.CDNSKEY): + raise ValueError("rdataset not a DNSKEY/CDNSKEY") + res = [] + for rdata in rdataset: + res.append(make_cds(name, rdata, algorithm, origin)) + return dns.rdataset.from_rdata_list(rdataset.ttl, res) + + +def dnskey_rdataset_to_cdnskey_rdataset( + rdataset: dns.rdataset.Rdataset, +) -> dns.rdataset.Rdataset: + """Create a CDNSKEY record from DNSKEY. + + *rdataset*, a ``dns.rdataset.Rdataset``, to create CDNSKEY Rdataset for. + + Returns a ``dns.rdataset.Rdataset`` + """ + + if rdataset.rdtype != dns.rdatatype.DNSKEY: + raise ValueError("rdataset not a DNSKEY") + res = [] + for rdata in rdataset: + res.append( + CDNSKEY( + rdclass=rdataset.rdclass, + rdtype=rdataset.rdtype, + flags=rdata.flags, + protocol=rdata.protocol, + algorithm=rdata.algorithm, + key=rdata.key, + ) + ) + return dns.rdataset.from_rdata_list(rdataset.ttl, res) + + +def default_rrset_signer( + txn: dns.transaction.Transaction, + rrset: dns.rrset.RRset, + signer: dns.name.Name, + ksks: List[Tuple[PrivateKey, DNSKEY]], + zsks: List[Tuple[PrivateKey, DNSKEY]], + inception: datetime | str | int | float | None = None, + expiration: datetime | str | int | float | None = None, + lifetime: int | None = None, + policy: Policy | None = None, + origin: dns.name.Name | None = None, + deterministic: bool = True, +) -> None: + """Default RRset signer""" + + if rrset.rdtype in set( + [ + dns.rdatatype.RdataType.DNSKEY, + dns.rdatatype.RdataType.CDS, + dns.rdatatype.RdataType.CDNSKEY, + ] + ): + keys = ksks + else: + keys = zsks + + for private_key, dnskey in keys: + rrsig = sign( + rrset=rrset, + private_key=private_key, + dnskey=dnskey, + inception=inception, + expiration=expiration, + lifetime=lifetime, + signer=signer, + policy=policy, + origin=origin, + deterministic=deterministic, + ) + txn.add(rrset.name, rrset.ttl, rrsig) + + +def sign_zone( + zone: dns.zone.Zone, + txn: dns.transaction.Transaction | None = None, + keys: List[Tuple[PrivateKey, DNSKEY]] | None = None, + add_dnskey: bool = True, + dnskey_ttl: int | None = None, + inception: datetime | str | int | float | None = None, + expiration: datetime | str | int | float | None = None, + lifetime: int | None = None, + nsec3: NSEC3PARAM | None = None, + rrset_signer: RRsetSigner | None = None, + policy: Policy | None = None, + deterministic: bool = True, +) -> None: + """Sign zone. + + *zone*, a ``dns.zone.Zone``, the zone to sign. + + *txn*, a ``dns.transaction.Transaction``, an optional transaction to use for + signing. + + *keys*, a list of (``PrivateKey``, ``DNSKEY``) tuples, to use for signing. KSK/ZSK + roles are assigned automatically if the SEP flag is used, otherwise all RRsets are + signed by all keys. + + *add_dnskey*, a ``bool``. If ``True``, the default, all specified DNSKEYs are + automatically added to the zone on signing. + + *dnskey_ttl*, a``int``, specifies the TTL for DNSKEY RRs. If not specified the TTL + of the existing DNSKEY RRset used or the TTL of the SOA RRset. + + *inception*, a ``datetime``, ``str``, ``int``, ``float`` or ``None``, the signature + inception time. If ``None``, the current time is used. If a ``str``, the format is + "YYYYMMDDHHMMSS" or alternatively the number of seconds since the UNIX epoch in text + form; this is the same the RRSIG rdata's text form. Values of type `int` or `float` + are interpreted as seconds since the UNIX epoch. + + *expiration*, a ``datetime``, ``str``, ``int``, ``float`` or ``None``, the signature + expiration time. If ``None``, the expiration time will be the inception time plus + the value of the *lifetime* parameter. See the description of *inception* above for + how the various parameter types are interpreted. + + *lifetime*, an ``int`` or ``None``, the signature lifetime in seconds. This + parameter is only meaningful if *expiration* is ``None``. + + *nsec3*, a ``NSEC3PARAM`` Rdata, configures signing using NSEC3. Not yet + implemented. + + *rrset_signer*, a ``Callable``, an optional function for signing RRsets. The + function requires two arguments: transaction and RRset. If the not specified, + ``dns.dnssec.default_rrset_signer`` will be used. + + *deterministic*, a ``bool``. If ``True``, the default, use deterministic + (reproducible) signatures when supported by the algorithm used for signing. + Currently, this only affects ECDSA. + + Returns ``None``. + """ + + ksks = [] + zsks = [] + + # if we have both KSKs and ZSKs, split by SEP flag. if not, sign all + # records with all keys + if keys: + for key in keys: + if key[1].flags & Flag.SEP: + ksks.append(key) + else: + zsks.append(key) + if not ksks: + ksks = keys + if not zsks: + zsks = keys + else: + keys = [] + + if txn: + cm: contextlib.AbstractContextManager = contextlib.nullcontext(txn) + else: + cm = zone.writer() + + if zone.origin is None: + raise ValueError("no zone origin") + + with cm as _txn: + if add_dnskey: + if dnskey_ttl is None: + dnskey = _txn.get(zone.origin, dns.rdatatype.DNSKEY) + if dnskey: + dnskey_ttl = dnskey.ttl + else: + soa = _txn.get(zone.origin, dns.rdatatype.SOA) + dnskey_ttl = soa.ttl + for _, dnskey in keys: + _txn.add(zone.origin, dnskey_ttl, dnskey) + + if nsec3: + raise NotImplementedError("Signing with NSEC3 not yet implemented") + else: + _rrset_signer = rrset_signer or functools.partial( + default_rrset_signer, + signer=zone.origin, + ksks=ksks, + zsks=zsks, + inception=inception, + expiration=expiration, + lifetime=lifetime, + policy=policy, + origin=zone.origin, + deterministic=deterministic, + ) + return _sign_zone_nsec(zone, _txn, _rrset_signer) + + +def _sign_zone_nsec( + zone: dns.zone.Zone, + txn: dns.transaction.Transaction, + rrset_signer: RRsetSigner | None = None, +) -> None: + """NSEC zone signer""" + + def _txn_add_nsec( + txn: dns.transaction.Transaction, + name: dns.name.Name, + next_secure: dns.name.Name | None, + rdclass: dns.rdataclass.RdataClass, + ttl: int, + rrset_signer: RRsetSigner | None = None, + ) -> None: + """NSEC zone signer helper""" + mandatory_types = set( + [dns.rdatatype.RdataType.RRSIG, dns.rdatatype.RdataType.NSEC] + ) + node = txn.get_node(name) + if node and next_secure: + types = ( + set([rdataset.rdtype for rdataset in node.rdatasets]) | mandatory_types + ) + windows = Bitmap.from_rdtypes(list(types)) + rrset = dns.rrset.from_rdata( + name, + ttl, + NSEC( + rdclass=rdclass, + rdtype=dns.rdatatype.RdataType.NSEC, + next=next_secure, + windows=windows, + ), + ) + txn.add(rrset) + if rrset_signer: + rrset_signer(txn, rrset) + + rrsig_ttl = zone.get_soa(txn).minimum + delegation = None + last_secure = None + + for name in sorted(txn.iterate_names()): + if delegation and name.is_subdomain(delegation): + # names below delegations are not secure + continue + elif txn.get(name, dns.rdatatype.NS) and name != zone.origin: + # inside delegation + delegation = name + else: + # outside delegation + delegation = None + + if rrset_signer: + node = txn.get_node(name) + if node: + for rdataset in node.rdatasets: + if rdataset.rdtype == dns.rdatatype.RRSIG: + # do not sign RRSIGs + continue + elif delegation and rdataset.rdtype != dns.rdatatype.DS: + # do not sign delegations except DS records + continue + else: + rrset = dns.rrset.from_rdata(name, rdataset.ttl, *rdataset) + rrset_signer(txn, rrset) + + # We need "is not None" as the empty name is False because its length is 0. + if last_secure is not None: + _txn_add_nsec(txn, last_secure, name, zone.rdclass, rrsig_ttl, rrset_signer) + last_secure = name + + if last_secure: + _txn_add_nsec( + txn, last_secure, zone.origin, zone.rdclass, rrsig_ttl, rrset_signer + ) + + +def _need_pyca(*args, **kwargs): + raise ImportError( + "DNSSEC validation requires python cryptography" + ) # pragma: no cover + + +if dns._features.have("dnssec"): + from cryptography.exceptions import InvalidSignature + from cryptography.hazmat.primitives.asymmetric import ec # pylint: disable=W0611 + from cryptography.hazmat.primitives.asymmetric import ed448 # pylint: disable=W0611 + from cryptography.hazmat.primitives.asymmetric import rsa # pylint: disable=W0611 + from cryptography.hazmat.primitives.asymmetric import ( # pylint: disable=W0611 + ed25519, + ) + + from dns.dnssecalgs import ( # pylint: disable=C0412 + get_algorithm_cls, + get_algorithm_cls_from_dnskey, + ) + from dns.dnssecalgs.base import GenericPrivateKey, GenericPublicKey + + validate = _validate # type: ignore + validate_rrsig = _validate_rrsig # type: ignore + sign = _sign + make_dnskey = _make_dnskey + make_cdnskey = _make_cdnskey + _have_pyca = True +else: # pragma: no cover + validate = _need_pyca + validate_rrsig = _need_pyca + sign = _need_pyca + make_dnskey = _need_pyca + make_cdnskey = _need_pyca + _have_pyca = False + +### BEGIN generated Algorithm constants + +RSAMD5 = Algorithm.RSAMD5 +DH = Algorithm.DH +DSA = Algorithm.DSA +ECC = Algorithm.ECC +RSASHA1 = Algorithm.RSASHA1 +DSANSEC3SHA1 = Algorithm.DSANSEC3SHA1 +RSASHA1NSEC3SHA1 = Algorithm.RSASHA1NSEC3SHA1 +RSASHA256 = Algorithm.RSASHA256 +RSASHA512 = Algorithm.RSASHA512 +ECCGOST = Algorithm.ECCGOST +ECDSAP256SHA256 = Algorithm.ECDSAP256SHA256 +ECDSAP384SHA384 = Algorithm.ECDSAP384SHA384 +ED25519 = Algorithm.ED25519 +ED448 = Algorithm.ED448 +INDIRECT = Algorithm.INDIRECT +PRIVATEDNS = Algorithm.PRIVATEDNS +PRIVATEOID = Algorithm.PRIVATEOID + +### END generated Algorithm constants diff --git a/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/__init__.py b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/__init__.py new file mode 100644 index 0000000..0810b19 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/__init__.py @@ -0,0 +1,124 @@ +from typing import Dict, Tuple, Type + +import dns._features +import dns.name +from dns.dnssecalgs.base import GenericPrivateKey +from dns.dnssectypes import Algorithm +from dns.exception import UnsupportedAlgorithm +from dns.rdtypes.ANY.DNSKEY import DNSKEY + +# pyright: reportPossiblyUnboundVariable=false + +if dns._features.have("dnssec"): + from dns.dnssecalgs.dsa import PrivateDSA, PrivateDSANSEC3SHA1 + from dns.dnssecalgs.ecdsa import PrivateECDSAP256SHA256, PrivateECDSAP384SHA384 + from dns.dnssecalgs.eddsa import PrivateED448, PrivateED25519 + from dns.dnssecalgs.rsa import ( + PrivateRSAMD5, + PrivateRSASHA1, + PrivateRSASHA1NSEC3SHA1, + PrivateRSASHA256, + PrivateRSASHA512, + ) + + _have_cryptography = True +else: + _have_cryptography = False + +AlgorithmPrefix = bytes | dns.name.Name | None + +algorithms: Dict[Tuple[Algorithm, AlgorithmPrefix], Type[GenericPrivateKey]] = {} +if _have_cryptography: + # pylint: disable=possibly-used-before-assignment + algorithms.update( + { + (Algorithm.RSAMD5, None): PrivateRSAMD5, + (Algorithm.DSA, None): PrivateDSA, + (Algorithm.RSASHA1, None): PrivateRSASHA1, + (Algorithm.DSANSEC3SHA1, None): PrivateDSANSEC3SHA1, + (Algorithm.RSASHA1NSEC3SHA1, None): PrivateRSASHA1NSEC3SHA1, + (Algorithm.RSASHA256, None): PrivateRSASHA256, + (Algorithm.RSASHA512, None): PrivateRSASHA512, + (Algorithm.ECDSAP256SHA256, None): PrivateECDSAP256SHA256, + (Algorithm.ECDSAP384SHA384, None): PrivateECDSAP384SHA384, + (Algorithm.ED25519, None): PrivateED25519, + (Algorithm.ED448, None): PrivateED448, + } + ) + + +def get_algorithm_cls( + algorithm: int | str, prefix: AlgorithmPrefix = None +) -> Type[GenericPrivateKey]: + """Get Private Key class from Algorithm. + + *algorithm*, a ``str`` or ``int`` specifying the DNSKEY algorithm. + + Raises ``UnsupportedAlgorithm`` if the algorithm is unknown. + + Returns a ``dns.dnssecalgs.GenericPrivateKey`` + """ + algorithm = Algorithm.make(algorithm) + cls = algorithms.get((algorithm, prefix)) + if cls: + return cls + raise UnsupportedAlgorithm( + f'algorithm "{Algorithm.to_text(algorithm)}" not supported by dnspython' + ) + + +def get_algorithm_cls_from_dnskey(dnskey: DNSKEY) -> Type[GenericPrivateKey]: + """Get Private Key class from DNSKEY. + + *dnskey*, a ``DNSKEY`` to get Algorithm class for. + + Raises ``UnsupportedAlgorithm`` if the algorithm is unknown. + + Returns a ``dns.dnssecalgs.GenericPrivateKey`` + """ + prefix: AlgorithmPrefix = None + if dnskey.algorithm == Algorithm.PRIVATEDNS: + prefix, _ = dns.name.from_wire(dnskey.key, 0) + elif dnskey.algorithm == Algorithm.PRIVATEOID: + length = int(dnskey.key[0]) + prefix = dnskey.key[0 : length + 1] + return get_algorithm_cls(dnskey.algorithm, prefix) + + +def register_algorithm_cls( + algorithm: int | str, + algorithm_cls: Type[GenericPrivateKey], + name: dns.name.Name | str | None = None, + oid: bytes | None = None, +) -> None: + """Register Algorithm Private Key class. + + *algorithm*, a ``str`` or ``int`` specifying the DNSKEY algorithm. + + *algorithm_cls*: A `GenericPrivateKey` class. + + *name*, an optional ``dns.name.Name`` or ``str``, for for PRIVATEDNS algorithms. + + *oid*: an optional BER-encoded `bytes` for PRIVATEOID algorithms. + + Raises ``ValueError`` if a name or oid is specified incorrectly. + """ + if not issubclass(algorithm_cls, GenericPrivateKey): + raise TypeError("Invalid algorithm class") + algorithm = Algorithm.make(algorithm) + prefix: AlgorithmPrefix = None + if algorithm == Algorithm.PRIVATEDNS: + if name is None: + raise ValueError("Name required for PRIVATEDNS algorithms") + if isinstance(name, str): + name = dns.name.from_text(name) + prefix = name + elif algorithm == Algorithm.PRIVATEOID: + if oid is None: + raise ValueError("OID required for PRIVATEOID algorithms") + prefix = bytes([len(oid)]) + oid + elif name: + raise ValueError("Name only supported for PRIVATEDNS algorithm") + elif oid: + raise ValueError("OID only supported for PRIVATEOID algorithm") + algorithms[(algorithm, prefix)] = algorithm_cls diff --git a/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/base.py b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/base.py new file mode 100644 index 0000000..0334fe6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/base.py @@ -0,0 +1,89 @@ +from abc import ABC, abstractmethod # pylint: disable=no-name-in-module +from typing import Any, Type + +import dns.rdataclass +import dns.rdatatype +from dns.dnssectypes import Algorithm +from dns.exception import AlgorithmKeyMismatch +from dns.rdtypes.ANY.DNSKEY import DNSKEY +from dns.rdtypes.dnskeybase import Flag + + +class GenericPublicKey(ABC): + algorithm: Algorithm + + @abstractmethod + def __init__(self, key: Any) -> None: + pass + + @abstractmethod + def verify(self, signature: bytes, data: bytes) -> None: + """Verify signed DNSSEC data""" + + @abstractmethod + def encode_key_bytes(self) -> bytes: + """Encode key as bytes for DNSKEY""" + + @classmethod + def _ensure_algorithm_key_combination(cls, key: DNSKEY) -> None: + if key.algorithm != cls.algorithm: + raise AlgorithmKeyMismatch + + def to_dnskey(self, flags: int = Flag.ZONE, protocol: int = 3) -> DNSKEY: + """Return public key as DNSKEY""" + return DNSKEY( + rdclass=dns.rdataclass.IN, + rdtype=dns.rdatatype.DNSKEY, + flags=flags, + protocol=protocol, + algorithm=self.algorithm, + key=self.encode_key_bytes(), + ) + + @classmethod + @abstractmethod + def from_dnskey(cls, key: DNSKEY) -> "GenericPublicKey": + """Create public key from DNSKEY""" + + @classmethod + @abstractmethod + def from_pem(cls, public_pem: bytes) -> "GenericPublicKey": + """Create public key from PEM-encoded SubjectPublicKeyInfo as specified + in RFC 5280""" + + @abstractmethod + def to_pem(self) -> bytes: + """Return public-key as PEM-encoded SubjectPublicKeyInfo as specified + in RFC 5280""" + + +class GenericPrivateKey(ABC): + public_cls: Type[GenericPublicKey] + + @abstractmethod + def __init__(self, key: Any) -> None: + pass + + @abstractmethod + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: + """Sign DNSSEC data""" + + @abstractmethod + def public_key(self) -> "GenericPublicKey": + """Return public key instance""" + + @classmethod + @abstractmethod + def from_pem( + cls, private_pem: bytes, password: bytes | None = None + ) -> "GenericPrivateKey": + """Create private key from PEM-encoded PKCS#8""" + + @abstractmethod + def to_pem(self, password: bytes | None = None) -> bytes: + """Return private key as PEM-encoded PKCS#8""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/cryptography.py b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/cryptography.py new file mode 100644 index 0000000..a5dde6a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/cryptography.py @@ -0,0 +1,68 @@ +from typing import Any, Type + +from cryptography.hazmat.primitives import serialization + +from dns.dnssecalgs.base import GenericPrivateKey, GenericPublicKey +from dns.exception import AlgorithmKeyMismatch + + +class CryptographyPublicKey(GenericPublicKey): + key: Any = None + key_cls: Any = None + + def __init__(self, key: Any) -> None: # pylint: disable=super-init-not-called + if self.key_cls is None: + raise TypeError("Undefined private key class") + if not isinstance( # pylint: disable=isinstance-second-argument-not-valid-type + key, self.key_cls + ): + raise AlgorithmKeyMismatch + self.key = key + + @classmethod + def from_pem(cls, public_pem: bytes) -> "GenericPublicKey": + key = serialization.load_pem_public_key(public_pem) + return cls(key=key) + + def to_pem(self) -> bytes: + return self.key.public_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PublicFormat.SubjectPublicKeyInfo, + ) + + +class CryptographyPrivateKey(GenericPrivateKey): + key: Any = None + key_cls: Any = None + public_cls: Type[CryptographyPublicKey] # pyright: ignore + + def __init__(self, key: Any) -> None: # pylint: disable=super-init-not-called + if self.key_cls is None: + raise TypeError("Undefined private key class") + if not isinstance( # pylint: disable=isinstance-second-argument-not-valid-type + key, self.key_cls + ): + raise AlgorithmKeyMismatch + self.key = key + + def public_key(self) -> "CryptographyPublicKey": + return self.public_cls(key=self.key.public_key()) + + @classmethod + def from_pem( + cls, private_pem: bytes, password: bytes | None = None + ) -> "GenericPrivateKey": + key = serialization.load_pem_private_key(private_pem, password=password) + return cls(key=key) + + def to_pem(self, password: bytes | None = None) -> bytes: + encryption_algorithm: serialization.KeySerializationEncryption + if password: + encryption_algorithm = serialization.BestAvailableEncryption(password) + else: + encryption_algorithm = serialization.NoEncryption() + return self.key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.PKCS8, + encryption_algorithm=encryption_algorithm, + ) diff --git a/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/dsa.py b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/dsa.py new file mode 100644 index 0000000..a4eb987 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/dsa.py @@ -0,0 +1,108 @@ +import struct + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import dsa, utils + +from dns.dnssecalgs.cryptography import CryptographyPrivateKey, CryptographyPublicKey +from dns.dnssectypes import Algorithm +from dns.rdtypes.ANY.DNSKEY import DNSKEY + + +class PublicDSA(CryptographyPublicKey): + key: dsa.DSAPublicKey + key_cls = dsa.DSAPublicKey + algorithm = Algorithm.DSA + chosen_hash = hashes.SHA1() + + def verify(self, signature: bytes, data: bytes) -> None: + sig_r = signature[1:21] + sig_s = signature[21:] + sig = utils.encode_dss_signature( + int.from_bytes(sig_r, "big"), int.from_bytes(sig_s, "big") + ) + self.key.verify(sig, data, self.chosen_hash) + + def encode_key_bytes(self) -> bytes: + """Encode a public key per RFC 2536, section 2.""" + pn = self.key.public_numbers() + dsa_t = (self.key.key_size // 8 - 64) // 8 + if dsa_t > 8: + raise ValueError("unsupported DSA key size") + octets = 64 + dsa_t * 8 + res = struct.pack("!B", dsa_t) + res += pn.parameter_numbers.q.to_bytes(20, "big") + res += pn.parameter_numbers.p.to_bytes(octets, "big") + res += pn.parameter_numbers.g.to_bytes(octets, "big") + res += pn.y.to_bytes(octets, "big") + return res + + @classmethod + def from_dnskey(cls, key: DNSKEY) -> "PublicDSA": + cls._ensure_algorithm_key_combination(key) + keyptr = key.key + (t,) = struct.unpack("!B", keyptr[0:1]) + keyptr = keyptr[1:] + octets = 64 + t * 8 + dsa_q = keyptr[0:20] + keyptr = keyptr[20:] + dsa_p = keyptr[0:octets] + keyptr = keyptr[octets:] + dsa_g = keyptr[0:octets] + keyptr = keyptr[octets:] + dsa_y = keyptr[0:octets] + return cls( + key=dsa.DSAPublicNumbers( # type: ignore + int.from_bytes(dsa_y, "big"), + dsa.DSAParameterNumbers( + int.from_bytes(dsa_p, "big"), + int.from_bytes(dsa_q, "big"), + int.from_bytes(dsa_g, "big"), + ), + ).public_key(default_backend()), + ) + + +class PrivateDSA(CryptographyPrivateKey): + key: dsa.DSAPrivateKey + key_cls = dsa.DSAPrivateKey + public_cls = PublicDSA + + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: + """Sign using a private key per RFC 2536, section 3.""" + public_dsa_key = self.key.public_key() + if public_dsa_key.key_size > 1024: + raise ValueError("DSA key size overflow") + der_signature = self.key.sign( + data, self.public_cls.chosen_hash # pyright: ignore + ) + dsa_r, dsa_s = utils.decode_dss_signature(der_signature) + dsa_t = (public_dsa_key.key_size // 8 - 64) // 8 + octets = 20 + signature = ( + struct.pack("!B", dsa_t) + + int.to_bytes(dsa_r, length=octets, byteorder="big") + + int.to_bytes(dsa_s, length=octets, byteorder="big") + ) + if verify: + self.public_key().verify(signature, data) + return signature + + @classmethod + def generate(cls, key_size: int) -> "PrivateDSA": + return cls( + key=dsa.generate_private_key(key_size=key_size), + ) + + +class PublicDSANSEC3SHA1(PublicDSA): + algorithm = Algorithm.DSANSEC3SHA1 + + +class PrivateDSANSEC3SHA1(PrivateDSA): + public_cls = PublicDSANSEC3SHA1 diff --git a/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/ecdsa.py b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/ecdsa.py new file mode 100644 index 0000000..e3f3f06 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/ecdsa.py @@ -0,0 +1,100 @@ +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import ec, utils + +from dns.dnssecalgs.cryptography import CryptographyPrivateKey, CryptographyPublicKey +from dns.dnssectypes import Algorithm +from dns.rdtypes.ANY.DNSKEY import DNSKEY + + +class PublicECDSA(CryptographyPublicKey): + key: ec.EllipticCurvePublicKey + key_cls = ec.EllipticCurvePublicKey + algorithm: Algorithm + chosen_hash: hashes.HashAlgorithm + curve: ec.EllipticCurve + octets: int + + def verify(self, signature: bytes, data: bytes) -> None: + sig_r = signature[0 : self.octets] + sig_s = signature[self.octets :] + sig = utils.encode_dss_signature( + int.from_bytes(sig_r, "big"), int.from_bytes(sig_s, "big") + ) + self.key.verify(sig, data, ec.ECDSA(self.chosen_hash)) + + def encode_key_bytes(self) -> bytes: + """Encode a public key per RFC 6605, section 4.""" + pn = self.key.public_numbers() + return pn.x.to_bytes(self.octets, "big") + pn.y.to_bytes(self.octets, "big") + + @classmethod + def from_dnskey(cls, key: DNSKEY) -> "PublicECDSA": + cls._ensure_algorithm_key_combination(key) + ecdsa_x = key.key[0 : cls.octets] + ecdsa_y = key.key[cls.octets : cls.octets * 2] + return cls( + key=ec.EllipticCurvePublicNumbers( + curve=cls.curve, + x=int.from_bytes(ecdsa_x, "big"), + y=int.from_bytes(ecdsa_y, "big"), + ).public_key(default_backend()), + ) + + +class PrivateECDSA(CryptographyPrivateKey): + key: ec.EllipticCurvePrivateKey + key_cls = ec.EllipticCurvePrivateKey + public_cls = PublicECDSA + + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: + """Sign using a private key per RFC 6605, section 4.""" + algorithm = ec.ECDSA( + self.public_cls.chosen_hash, # pyright: ignore + deterministic_signing=deterministic, + ) + der_signature = self.key.sign(data, algorithm) + dsa_r, dsa_s = utils.decode_dss_signature(der_signature) + signature = int.to_bytes( + dsa_r, length=self.public_cls.octets, byteorder="big" # pyright: ignore + ) + int.to_bytes( + dsa_s, length=self.public_cls.octets, byteorder="big" # pyright: ignore + ) + if verify: + self.public_key().verify(signature, data) + return signature + + @classmethod + def generate(cls) -> "PrivateECDSA": + return cls( + key=ec.generate_private_key( + curve=cls.public_cls.curve, backend=default_backend() # pyright: ignore + ), + ) + + +class PublicECDSAP256SHA256(PublicECDSA): + algorithm = Algorithm.ECDSAP256SHA256 + chosen_hash = hashes.SHA256() + curve = ec.SECP256R1() + octets = 32 + + +class PrivateECDSAP256SHA256(PrivateECDSA): + public_cls = PublicECDSAP256SHA256 + + +class PublicECDSAP384SHA384(PublicECDSA): + algorithm = Algorithm.ECDSAP384SHA384 + chosen_hash = hashes.SHA384() + curve = ec.SECP384R1() + octets = 48 + + +class PrivateECDSAP384SHA384(PrivateECDSA): + public_cls = PublicECDSAP384SHA384 diff --git a/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/eddsa.py b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/eddsa.py new file mode 100644 index 0000000..1cbb407 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/eddsa.py @@ -0,0 +1,70 @@ +from typing import Type + +from cryptography.hazmat.primitives import serialization +from cryptography.hazmat.primitives.asymmetric import ed448, ed25519 + +from dns.dnssecalgs.cryptography import CryptographyPrivateKey, CryptographyPublicKey +from dns.dnssectypes import Algorithm +from dns.rdtypes.ANY.DNSKEY import DNSKEY + + +class PublicEDDSA(CryptographyPublicKey): + def verify(self, signature: bytes, data: bytes) -> None: + self.key.verify(signature, data) + + def encode_key_bytes(self) -> bytes: + """Encode a public key per RFC 8080, section 3.""" + return self.key.public_bytes( + encoding=serialization.Encoding.Raw, format=serialization.PublicFormat.Raw + ) + + @classmethod + def from_dnskey(cls, key: DNSKEY) -> "PublicEDDSA": + cls._ensure_algorithm_key_combination(key) + return cls( + key=cls.key_cls.from_public_bytes(key.key), + ) + + +class PrivateEDDSA(CryptographyPrivateKey): + public_cls: Type[PublicEDDSA] # pyright: ignore + + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: + """Sign using a private key per RFC 8080, section 4.""" + signature = self.key.sign(data) + if verify: + self.public_key().verify(signature, data) + return signature + + @classmethod + def generate(cls) -> "PrivateEDDSA": + return cls(key=cls.key_cls.generate()) + + +class PublicED25519(PublicEDDSA): + key: ed25519.Ed25519PublicKey + key_cls = ed25519.Ed25519PublicKey + algorithm = Algorithm.ED25519 + + +class PrivateED25519(PrivateEDDSA): + key: ed25519.Ed25519PrivateKey + key_cls = ed25519.Ed25519PrivateKey + public_cls = PublicED25519 + + +class PublicED448(PublicEDDSA): + key: ed448.Ed448PublicKey + key_cls = ed448.Ed448PublicKey + algorithm = Algorithm.ED448 + + +class PrivateED448(PrivateEDDSA): + key: ed448.Ed448PrivateKey + key_cls = ed448.Ed448PrivateKey + public_cls = PublicED448 diff --git a/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/rsa.py b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/rsa.py new file mode 100644 index 0000000..de9160b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/dnssecalgs/rsa.py @@ -0,0 +1,126 @@ +import math +import struct + +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes +from cryptography.hazmat.primitives.asymmetric import padding, rsa + +from dns.dnssecalgs.cryptography import CryptographyPrivateKey, CryptographyPublicKey +from dns.dnssectypes import Algorithm +from dns.rdtypes.ANY.DNSKEY import DNSKEY + + +class PublicRSA(CryptographyPublicKey): + key: rsa.RSAPublicKey + key_cls = rsa.RSAPublicKey + algorithm: Algorithm + chosen_hash: hashes.HashAlgorithm + + def verify(self, signature: bytes, data: bytes) -> None: + self.key.verify(signature, data, padding.PKCS1v15(), self.chosen_hash) + + def encode_key_bytes(self) -> bytes: + """Encode a public key per RFC 3110, section 2.""" + pn = self.key.public_numbers() + _exp_len = math.ceil(int.bit_length(pn.e) / 8) + exp = int.to_bytes(pn.e, length=_exp_len, byteorder="big") + if _exp_len > 255: + exp_header = b"\0" + struct.pack("!H", _exp_len) + else: + exp_header = struct.pack("!B", _exp_len) + if pn.n.bit_length() < 512 or pn.n.bit_length() > 4096: + raise ValueError("unsupported RSA key length") + return exp_header + exp + pn.n.to_bytes((pn.n.bit_length() + 7) // 8, "big") + + @classmethod + def from_dnskey(cls, key: DNSKEY) -> "PublicRSA": + cls._ensure_algorithm_key_combination(key) + keyptr = key.key + (bytes_,) = struct.unpack("!B", keyptr[0:1]) + keyptr = keyptr[1:] + if bytes_ == 0: + (bytes_,) = struct.unpack("!H", keyptr[0:2]) + keyptr = keyptr[2:] + rsa_e = keyptr[0:bytes_] + rsa_n = keyptr[bytes_:] + return cls( + key=rsa.RSAPublicNumbers( + int.from_bytes(rsa_e, "big"), int.from_bytes(rsa_n, "big") + ).public_key(default_backend()) + ) + + +class PrivateRSA(CryptographyPrivateKey): + key: rsa.RSAPrivateKey + key_cls = rsa.RSAPrivateKey + public_cls = PublicRSA + default_public_exponent = 65537 + + def sign( + self, + data: bytes, + verify: bool = False, + deterministic: bool = True, + ) -> bytes: + """Sign using a private key per RFC 3110, section 3.""" + signature = self.key.sign( + data, padding.PKCS1v15(), self.public_cls.chosen_hash # pyright: ignore + ) + if verify: + self.public_key().verify(signature, data) + return signature + + @classmethod + def generate(cls, key_size: int) -> "PrivateRSA": + return cls( + key=rsa.generate_private_key( + public_exponent=cls.default_public_exponent, + key_size=key_size, + backend=default_backend(), + ) + ) + + +class PublicRSAMD5(PublicRSA): + algorithm = Algorithm.RSAMD5 + chosen_hash = hashes.MD5() + + +class PrivateRSAMD5(PrivateRSA): + public_cls = PublicRSAMD5 + + +class PublicRSASHA1(PublicRSA): + algorithm = Algorithm.RSASHA1 + chosen_hash = hashes.SHA1() + + +class PrivateRSASHA1(PrivateRSA): + public_cls = PublicRSASHA1 + + +class PublicRSASHA1NSEC3SHA1(PublicRSA): + algorithm = Algorithm.RSASHA1NSEC3SHA1 + chosen_hash = hashes.SHA1() + + +class PrivateRSASHA1NSEC3SHA1(PrivateRSA): + public_cls = PublicRSASHA1NSEC3SHA1 + + +class PublicRSASHA256(PublicRSA): + algorithm = Algorithm.RSASHA256 + chosen_hash = hashes.SHA256() + + +class PrivateRSASHA256(PrivateRSA): + public_cls = PublicRSASHA256 + + +class PublicRSASHA512(PublicRSA): + algorithm = Algorithm.RSASHA512 + chosen_hash = hashes.SHA512() + + +class PrivateRSASHA512(PrivateRSA): + public_cls = PublicRSASHA512 diff --git a/netdeploy/lib/python3.11/site-packages/dns/dnssectypes.py b/netdeploy/lib/python3.11/site-packages/dns/dnssectypes.py new file mode 100644 index 0000000..02131e0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/dnssectypes.py @@ -0,0 +1,71 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Common DNSSEC-related types.""" + +# This is a separate file to avoid import circularity between dns.dnssec and +# the implementations of the DS and DNSKEY types. + +import dns.enum + + +class Algorithm(dns.enum.IntEnum): + RSAMD5 = 1 + DH = 2 + DSA = 3 + ECC = 4 + RSASHA1 = 5 + DSANSEC3SHA1 = 6 + RSASHA1NSEC3SHA1 = 7 + RSASHA256 = 8 + RSASHA512 = 10 + ECCGOST = 12 + ECDSAP256SHA256 = 13 + ECDSAP384SHA384 = 14 + ED25519 = 15 + ED448 = 16 + INDIRECT = 252 + PRIVATEDNS = 253 + PRIVATEOID = 254 + + @classmethod + def _maximum(cls): + return 255 + + +class DSDigest(dns.enum.IntEnum): + """DNSSEC Delegation Signer Digest Algorithm""" + + NULL = 0 + SHA1 = 1 + SHA256 = 2 + GOST = 3 + SHA384 = 4 + + @classmethod + def _maximum(cls): + return 255 + + +class NSEC3Hash(dns.enum.IntEnum): + """NSEC3 hash algorithm""" + + SHA1 = 1 + + @classmethod + def _maximum(cls): + return 255 diff --git a/netdeploy/lib/python3.11/site-packages/dns/e164.py b/netdeploy/lib/python3.11/site-packages/dns/e164.py new file mode 100644 index 0000000..942d2c0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/e164.py @@ -0,0 +1,116 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2006-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS E.164 helpers.""" + +from typing import Iterable + +import dns.exception +import dns.name +import dns.resolver + +#: The public E.164 domain. +public_enum_domain = dns.name.from_text("e164.arpa.") + + +def from_e164( + text: str, origin: dns.name.Name | None = public_enum_domain +) -> dns.name.Name: + """Convert an E.164 number in textual form into a Name object whose + value is the ENUM domain name for that number. + + Non-digits in the text are ignored, i.e. "16505551212", + "+1.650.555.1212" and "1 (650) 555-1212" are all the same. + + *text*, a ``str``, is an E.164 number in textual form. + + *origin*, a ``dns.name.Name``, the domain in which the number + should be constructed. The default is ``e164.arpa.``. + + Returns a ``dns.name.Name``. + """ + + parts = [d for d in text if d.isdigit()] + parts.reverse() + return dns.name.from_text(".".join(parts), origin=origin) + + +def to_e164( + name: dns.name.Name, + origin: dns.name.Name | None = public_enum_domain, + want_plus_prefix: bool = True, +) -> str: + """Convert an ENUM domain name into an E.164 number. + + Note that dnspython does not have any information about preferred + number formats within national numbering plans, so all numbers are + emitted as a simple string of digits, prefixed by a '+' (unless + *want_plus_prefix* is ``False``). + + *name* is a ``dns.name.Name``, the ENUM domain name. + + *origin* is a ``dns.name.Name``, a domain containing the ENUM + domain name. The name is relativized to this domain before being + converted to text. If ``None``, no relativization is done. + + *want_plus_prefix* is a ``bool``. If True, add a '+' to the beginning of + the returned number. + + Returns a ``str``. + + """ + if origin is not None: + name = name.relativize(origin) + dlabels = [d for d in name.labels if d.isdigit() and len(d) == 1] + if len(dlabels) != len(name.labels): + raise dns.exception.SyntaxError("non-digit labels in ENUM domain name") + dlabels.reverse() + text = b"".join(dlabels) + if want_plus_prefix: + text = b"+" + text + return text.decode() + + +def query( + number: str, + domains: Iterable[dns.name.Name | str], + resolver: dns.resolver.Resolver | None = None, +) -> dns.resolver.Answer: + """Look for NAPTR RRs for the specified number in the specified domains. + + e.g. lookup('16505551212', ['e164.dnspython.org.', 'e164.arpa.']) + + *number*, a ``str`` is the number to look for. + + *domains* is an iterable containing ``dns.name.Name`` values. + + *resolver*, a ``dns.resolver.Resolver``, is the resolver to use. If + ``None``, the default resolver is used. + """ + + if resolver is None: + resolver = dns.resolver.get_default_resolver() + e_nx = dns.resolver.NXDOMAIN() + for domain in domains: + if isinstance(domain, str): + domain = dns.name.from_text(domain) + qname = from_e164(number, domain) + try: + return resolver.resolve(qname, "NAPTR") + except dns.resolver.NXDOMAIN as e: + e_nx += e + raise e_nx diff --git a/netdeploy/lib/python3.11/site-packages/dns/edns.py b/netdeploy/lib/python3.11/site-packages/dns/edns.py new file mode 100644 index 0000000..eb98548 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/edns.py @@ -0,0 +1,591 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2009-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""EDNS Options""" + +import binascii +import math +import socket +import struct +from typing import Any, Dict + +import dns.enum +import dns.inet +import dns.ipv4 +import dns.ipv6 +import dns.name +import dns.rdata +import dns.wire + + +class OptionType(dns.enum.IntEnum): + """EDNS option type codes""" + + #: NSID + NSID = 3 + #: DAU + DAU = 5 + #: DHU + DHU = 6 + #: N3U + N3U = 7 + #: ECS (client-subnet) + ECS = 8 + #: EXPIRE + EXPIRE = 9 + #: COOKIE + COOKIE = 10 + #: KEEPALIVE + KEEPALIVE = 11 + #: PADDING + PADDING = 12 + #: CHAIN + CHAIN = 13 + #: EDE (extended-dns-error) + EDE = 15 + #: REPORTCHANNEL + REPORTCHANNEL = 18 + + @classmethod + def _maximum(cls): + return 65535 + + +class Option: + """Base class for all EDNS option types.""" + + def __init__(self, otype: OptionType | str): + """Initialize an option. + + *otype*, a ``dns.edns.OptionType``, is the option type. + """ + self.otype = OptionType.make(otype) + + def to_wire(self, file: Any | None = None) -> bytes | None: + """Convert an option to wire format. + + Returns a ``bytes`` or ``None``. + + """ + raise NotImplementedError # pragma: no cover + + def to_text(self) -> str: + raise NotImplementedError # pragma: no cover + + def to_generic(self) -> "GenericOption": + """Creates a dns.edns.GenericOption equivalent of this rdata. + + Returns a ``dns.edns.GenericOption``. + """ + wire = self.to_wire() + assert wire is not None # for mypy + return GenericOption(self.otype, wire) + + @classmethod + def from_wire_parser(cls, otype: OptionType, parser: "dns.wire.Parser") -> "Option": + """Build an EDNS option object from wire format. + + *otype*, a ``dns.edns.OptionType``, is the option type. + + *parser*, a ``dns.wire.Parser``, the parser, which should be + restructed to the option length. + + Returns a ``dns.edns.Option``. + """ + raise NotImplementedError # pragma: no cover + + def _cmp(self, other): + """Compare an EDNS option with another option of the same type. + + Returns < 0 if < *other*, 0 if == *other*, and > 0 if > *other*. + """ + wire = self.to_wire() + owire = other.to_wire() + if wire == owire: + return 0 + if wire > owire: + return 1 + return -1 + + def __eq__(self, other): + if not isinstance(other, Option): + return False + if self.otype != other.otype: + return False + return self._cmp(other) == 0 + + def __ne__(self, other): + if not isinstance(other, Option): + return True + if self.otype != other.otype: + return True + return self._cmp(other) != 0 + + def __lt__(self, other): + if not isinstance(other, Option) or self.otype != other.otype: + return NotImplemented + return self._cmp(other) < 0 + + def __le__(self, other): + if not isinstance(other, Option) or self.otype != other.otype: + return NotImplemented + return self._cmp(other) <= 0 + + def __ge__(self, other): + if not isinstance(other, Option) or self.otype != other.otype: + return NotImplemented + return self._cmp(other) >= 0 + + def __gt__(self, other): + if not isinstance(other, Option) or self.otype != other.otype: + return NotImplemented + return self._cmp(other) > 0 + + def __str__(self): + return self.to_text() + + +class GenericOption(Option): # lgtm[py/missing-equals] + """Generic Option Class + + This class is used for EDNS option types for which we have no better + implementation. + """ + + def __init__(self, otype: OptionType | str, data: bytes | str): + super().__init__(otype) + self.data = dns.rdata.Rdata._as_bytes(data, True) + + def to_wire(self, file: Any | None = None) -> bytes | None: + if file: + file.write(self.data) + return None + else: + return self.data + + def to_text(self) -> str: + return f"Generic {self.otype}" + + def to_generic(self) -> "GenericOption": + return self + + @classmethod + def from_wire_parser( + cls, otype: OptionType | str, parser: "dns.wire.Parser" + ) -> Option: + return cls(otype, parser.get_remaining()) + + +class ECSOption(Option): # lgtm[py/missing-equals] + """EDNS Client Subnet (ECS, RFC7871)""" + + def __init__(self, address: str, srclen: int | None = None, scopelen: int = 0): + """*address*, a ``str``, is the client address information. + + *srclen*, an ``int``, the source prefix length, which is the + leftmost number of bits of the address to be used for the + lookup. The default is 24 for IPv4 and 56 for IPv6. + + *scopelen*, an ``int``, the scope prefix length. This value + must be 0 in queries, and should be set in responses. + """ + + super().__init__(OptionType.ECS) + af = dns.inet.af_for_address(address) + + if af == socket.AF_INET6: + self.family = 2 + if srclen is None: + srclen = 56 + address = dns.rdata.Rdata._as_ipv6_address(address) + srclen = dns.rdata.Rdata._as_int(srclen, 0, 128) + scopelen = dns.rdata.Rdata._as_int(scopelen, 0, 128) + elif af == socket.AF_INET: + self.family = 1 + if srclen is None: + srclen = 24 + address = dns.rdata.Rdata._as_ipv4_address(address) + srclen = dns.rdata.Rdata._as_int(srclen, 0, 32) + scopelen = dns.rdata.Rdata._as_int(scopelen, 0, 32) + else: # pragma: no cover (this will never happen) + raise ValueError("Bad address family") + + assert srclen is not None + self.address = address + self.srclen = srclen + self.scopelen = scopelen + + addrdata = dns.inet.inet_pton(af, address) + nbytes = int(math.ceil(srclen / 8.0)) + + # Truncate to srclen and pad to the end of the last octet needed + # See RFC section 6 + self.addrdata = addrdata[:nbytes] + nbits = srclen % 8 + if nbits != 0: + last = struct.pack("B", ord(self.addrdata[-1:]) & (0xFF << (8 - nbits))) + self.addrdata = self.addrdata[:-1] + last + + def to_text(self) -> str: + return f"ECS {self.address}/{self.srclen} scope/{self.scopelen}" + + @staticmethod + def from_text(text: str) -> Option: + """Convert a string into a `dns.edns.ECSOption` + + *text*, a `str`, the text form of the option. + + Returns a `dns.edns.ECSOption`. + + Examples: + + >>> import dns.edns + >>> + >>> # basic example + >>> dns.edns.ECSOption.from_text('1.2.3.4/24') + >>> + >>> # also understands scope + >>> dns.edns.ECSOption.from_text('1.2.3.4/24/32') + >>> + >>> # IPv6 + >>> dns.edns.ECSOption.from_text('2001:4b98::1/64/64') + >>> + >>> # it understands results from `dns.edns.ECSOption.to_text()` + >>> dns.edns.ECSOption.from_text('ECS 1.2.3.4/24/32') + """ + optional_prefix = "ECS" + tokens = text.split() + ecs_text = None + if len(tokens) == 1: + ecs_text = tokens[0] + elif len(tokens) == 2: + if tokens[0] != optional_prefix: + raise ValueError(f'could not parse ECS from "{text}"') + ecs_text = tokens[1] + else: + raise ValueError(f'could not parse ECS from "{text}"') + n_slashes = ecs_text.count("/") + if n_slashes == 1: + address, tsrclen = ecs_text.split("/") + tscope = "0" + elif n_slashes == 2: + address, tsrclen, tscope = ecs_text.split("/") + else: + raise ValueError(f'could not parse ECS from "{text}"') + try: + scope = int(tscope) + except ValueError: + raise ValueError("invalid scope " + f'"{tscope}": scope must be an integer') + try: + srclen = int(tsrclen) + except ValueError: + raise ValueError( + "invalid srclen " + f'"{tsrclen}": srclen must be an integer' + ) + return ECSOption(address, srclen, scope) + + def to_wire(self, file: Any | None = None) -> bytes | None: + value = ( + struct.pack("!HBB", self.family, self.srclen, self.scopelen) + self.addrdata + ) + if file: + file.write(value) + return None + else: + return value + + @classmethod + def from_wire_parser( + cls, otype: OptionType | str, parser: "dns.wire.Parser" + ) -> Option: + family, src, scope = parser.get_struct("!HBB") + addrlen = int(math.ceil(src / 8.0)) + prefix = parser.get_bytes(addrlen) + if family == 1: + pad = 4 - addrlen + addr = dns.ipv4.inet_ntoa(prefix + b"\x00" * pad) + elif family == 2: + pad = 16 - addrlen + addr = dns.ipv6.inet_ntoa(prefix + b"\x00" * pad) + else: + raise ValueError("unsupported family") + + return cls(addr, src, scope) + + +class EDECode(dns.enum.IntEnum): + """Extended DNS Error (EDE) codes""" + + OTHER = 0 + UNSUPPORTED_DNSKEY_ALGORITHM = 1 + UNSUPPORTED_DS_DIGEST_TYPE = 2 + STALE_ANSWER = 3 + FORGED_ANSWER = 4 + DNSSEC_INDETERMINATE = 5 + DNSSEC_BOGUS = 6 + SIGNATURE_EXPIRED = 7 + SIGNATURE_NOT_YET_VALID = 8 + DNSKEY_MISSING = 9 + RRSIGS_MISSING = 10 + NO_ZONE_KEY_BIT_SET = 11 + NSEC_MISSING = 12 + CACHED_ERROR = 13 + NOT_READY = 14 + BLOCKED = 15 + CENSORED = 16 + FILTERED = 17 + PROHIBITED = 18 + STALE_NXDOMAIN_ANSWER = 19 + NOT_AUTHORITATIVE = 20 + NOT_SUPPORTED = 21 + NO_REACHABLE_AUTHORITY = 22 + NETWORK_ERROR = 23 + INVALID_DATA = 24 + + @classmethod + def _maximum(cls): + return 65535 + + +class EDEOption(Option): # lgtm[py/missing-equals] + """Extended DNS Error (EDE, RFC8914)""" + + _preserve_case = {"DNSKEY", "DS", "DNSSEC", "RRSIGs", "NSEC", "NXDOMAIN"} + + def __init__(self, code: EDECode | str, text: str | None = None): + """*code*, a ``dns.edns.EDECode`` or ``str``, the info code of the + extended error. + + *text*, a ``str`` or ``None``, specifying additional information about + the error. + """ + + super().__init__(OptionType.EDE) + + self.code = EDECode.make(code) + if text is not None and not isinstance(text, str): + raise ValueError("text must be string or None") + self.text = text + + def to_text(self) -> str: + output = f"EDE {self.code}" + if self.code in EDECode: + desc = EDECode.to_text(self.code) + desc = " ".join( + word if word in self._preserve_case else word.title() + for word in desc.split("_") + ) + output += f" ({desc})" + if self.text is not None: + output += f": {self.text}" + return output + + def to_wire(self, file: Any | None = None) -> bytes | None: + value = struct.pack("!H", self.code) + if self.text is not None: + value += self.text.encode("utf8") + + if file: + file.write(value) + return None + else: + return value + + @classmethod + def from_wire_parser( + cls, otype: OptionType | str, parser: "dns.wire.Parser" + ) -> Option: + code = EDECode.make(parser.get_uint16()) + text = parser.get_remaining() + + if text: + if text[-1] == 0: # text MAY be null-terminated + text = text[:-1] + btext = text.decode("utf8") + else: + btext = None + + return cls(code, btext) + + +class NSIDOption(Option): + def __init__(self, nsid: bytes): + super().__init__(OptionType.NSID) + self.nsid = nsid + + def to_wire(self, file: Any = None) -> bytes | None: + if file: + file.write(self.nsid) + return None + else: + return self.nsid + + def to_text(self) -> str: + if all(c >= 0x20 and c <= 0x7E for c in self.nsid): + # All ASCII printable, so it's probably a string. + value = self.nsid.decode() + else: + value = binascii.hexlify(self.nsid).decode() + return f"NSID {value}" + + @classmethod + def from_wire_parser( + cls, otype: OptionType | str, parser: dns.wire.Parser + ) -> Option: + return cls(parser.get_remaining()) + + +class CookieOption(Option): + def __init__(self, client: bytes, server: bytes): + super().__init__(OptionType.COOKIE) + self.client = client + self.server = server + if len(client) != 8: + raise ValueError("client cookie must be 8 bytes") + if len(server) != 0 and (len(server) < 8 or len(server) > 32): + raise ValueError("server cookie must be empty or between 8 and 32 bytes") + + def to_wire(self, file: Any = None) -> bytes | None: + if file: + file.write(self.client) + if len(self.server) > 0: + file.write(self.server) + return None + else: + return self.client + self.server + + def to_text(self) -> str: + client = binascii.hexlify(self.client).decode() + if len(self.server) > 0: + server = binascii.hexlify(self.server).decode() + else: + server = "" + return f"COOKIE {client}{server}" + + @classmethod + def from_wire_parser( + cls, otype: OptionType | str, parser: dns.wire.Parser + ) -> Option: + return cls(parser.get_bytes(8), parser.get_remaining()) + + +class ReportChannelOption(Option): + # RFC 9567 + def __init__(self, agent_domain: dns.name.Name): + super().__init__(OptionType.REPORTCHANNEL) + self.agent_domain = agent_domain + + def to_wire(self, file: Any = None) -> bytes | None: + return self.agent_domain.to_wire(file) + + def to_text(self) -> str: + return "REPORTCHANNEL " + self.agent_domain.to_text() + + @classmethod + def from_wire_parser( + cls, otype: OptionType | str, parser: dns.wire.Parser + ) -> Option: + return cls(parser.get_name()) + + +_type_to_class: Dict[OptionType, Any] = { + OptionType.ECS: ECSOption, + OptionType.EDE: EDEOption, + OptionType.NSID: NSIDOption, + OptionType.COOKIE: CookieOption, + OptionType.REPORTCHANNEL: ReportChannelOption, +} + + +def get_option_class(otype: OptionType) -> Any: + """Return the class for the specified option type. + + The GenericOption class is used if a more specific class is not + known. + """ + + cls = _type_to_class.get(otype) + if cls is None: + cls = GenericOption + return cls + + +def option_from_wire_parser( + otype: OptionType | str, parser: "dns.wire.Parser" +) -> Option: + """Build an EDNS option object from wire format. + + *otype*, an ``int``, is the option type. + + *parser*, a ``dns.wire.Parser``, the parser, which should be + restricted to the option length. + + Returns an instance of a subclass of ``dns.edns.Option``. + """ + otype = OptionType.make(otype) + cls = get_option_class(otype) + return cls.from_wire_parser(otype, parser) + + +def option_from_wire( + otype: OptionType | str, wire: bytes, current: int, olen: int +) -> Option: + """Build an EDNS option object from wire format. + + *otype*, an ``int``, is the option type. + + *wire*, a ``bytes``, is the wire-format message. + + *current*, an ``int``, is the offset in *wire* of the beginning + of the rdata. + + *olen*, an ``int``, is the length of the wire-format option data + + Returns an instance of a subclass of ``dns.edns.Option``. + """ + parser = dns.wire.Parser(wire, current) + with parser.restrict_to(olen): + return option_from_wire_parser(otype, parser) + + +def register_type(implementation: Any, otype: OptionType) -> None: + """Register the implementation of an option type. + + *implementation*, a ``class``, is a subclass of ``dns.edns.Option``. + + *otype*, an ``int``, is the option type. + """ + + _type_to_class[otype] = implementation + + +### BEGIN generated OptionType constants + +NSID = OptionType.NSID +DAU = OptionType.DAU +DHU = OptionType.DHU +N3U = OptionType.N3U +ECS = OptionType.ECS +EXPIRE = OptionType.EXPIRE +COOKIE = OptionType.COOKIE +KEEPALIVE = OptionType.KEEPALIVE +PADDING = OptionType.PADDING +CHAIN = OptionType.CHAIN +EDE = OptionType.EDE +REPORTCHANNEL = OptionType.REPORTCHANNEL + +### END generated OptionType constants diff --git a/netdeploy/lib/python3.11/site-packages/dns/entropy.py b/netdeploy/lib/python3.11/site-packages/dns/entropy.py new file mode 100644 index 0000000..6430926 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/entropy.py @@ -0,0 +1,130 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2009-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import hashlib +import os +import random +import threading +import time +from typing import Any + + +class EntropyPool: + # This is an entropy pool for Python implementations that do not + # have a working SystemRandom. I'm not sure there are any, but + # leaving this code doesn't hurt anything as the library code + # is used if present. + + def __init__(self, seed: bytes | None = None): + self.pool_index = 0 + self.digest: bytearray | None = None + self.next_byte = 0 + self.lock = threading.Lock() + self.hash = hashlib.sha1() + self.hash_len = 20 + self.pool = bytearray(b"\0" * self.hash_len) + if seed is not None: + self._stir(seed) + self.seeded = True + self.seed_pid = os.getpid() + else: + self.seeded = False + self.seed_pid = 0 + + def _stir(self, entropy: bytes | bytearray) -> None: + for c in entropy: + if self.pool_index == self.hash_len: + self.pool_index = 0 + b = c & 0xFF + self.pool[self.pool_index] ^= b + self.pool_index += 1 + + def stir(self, entropy: bytes | bytearray) -> None: + with self.lock: + self._stir(entropy) + + def _maybe_seed(self) -> None: + if not self.seeded or self.seed_pid != os.getpid(): + try: + seed = os.urandom(16) + except Exception: # pragma: no cover + try: + with open("/dev/urandom", "rb", 0) as r: + seed = r.read(16) + except Exception: + seed = str(time.time()).encode() + self.seeded = True + self.seed_pid = os.getpid() + self.digest = None + seed = bytearray(seed) + self._stir(seed) + + def random_8(self) -> int: + with self.lock: + self._maybe_seed() + if self.digest is None or self.next_byte == self.hash_len: + self.hash.update(bytes(self.pool)) + self.digest = bytearray(self.hash.digest()) + self._stir(self.digest) + self.next_byte = 0 + value = self.digest[self.next_byte] + self.next_byte += 1 + return value + + def random_16(self) -> int: + return self.random_8() * 256 + self.random_8() + + def random_32(self) -> int: + return self.random_16() * 65536 + self.random_16() + + def random_between(self, first: int, last: int) -> int: + size = last - first + 1 + if size > 4294967296: + raise ValueError("too big") + if size > 65536: + rand = self.random_32 + max = 4294967295 + elif size > 256: + rand = self.random_16 + max = 65535 + else: + rand = self.random_8 + max = 255 + return first + size * rand() // (max + 1) + + +pool = EntropyPool() + +system_random: Any | None +try: + system_random = random.SystemRandom() +except Exception: # pragma: no cover + system_random = None + + +def random_16() -> int: + if system_random is not None: + return system_random.randrange(0, 65536) + else: + return pool.random_16() + + +def between(first: int, last: int) -> int: + if system_random is not None: + return system_random.randrange(first, last + 1) + else: + return pool.random_between(first, last) diff --git a/netdeploy/lib/python3.11/site-packages/dns/enum.py b/netdeploy/lib/python3.11/site-packages/dns/enum.py new file mode 100644 index 0000000..822c995 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/enum.py @@ -0,0 +1,113 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import enum +from typing import Any, Type, TypeVar + +TIntEnum = TypeVar("TIntEnum", bound="IntEnum") + + +class IntEnum(enum.IntEnum): + @classmethod + def _missing_(cls, value): + cls._check_value(value) + val = int.__new__(cls, value) # pyright: ignore + val._name_ = cls._extra_to_text(value, None) or f"{cls._prefix()}{value}" + val._value_ = value # pyright: ignore + return val + + @classmethod + def _check_value(cls, value): + max = cls._maximum() + if not isinstance(value, int): + raise TypeError + if value < 0 or value > max: + name = cls._short_name() + raise ValueError(f"{name} must be an int between >= 0 and <= {max}") + + @classmethod + def from_text(cls: Type[TIntEnum], text: str) -> TIntEnum: + text = text.upper() + try: + return cls[text] + except KeyError: + pass + value = cls._extra_from_text(text) + if value: + return value + prefix = cls._prefix() + if text.startswith(prefix) and text[len(prefix) :].isdigit(): + value = int(text[len(prefix) :]) + cls._check_value(value) + return cls(value) + raise cls._unknown_exception_class() + + @classmethod + def to_text(cls: Type[TIntEnum], value: int) -> str: + cls._check_value(value) + try: + text = cls(value).name + except ValueError: + text = None + text = cls._extra_to_text(value, text) + if text is None: + text = f"{cls._prefix()}{value}" + return text + + @classmethod + def make(cls: Type[TIntEnum], value: int | str) -> TIntEnum: + """Convert text or a value into an enumerated type, if possible. + + *value*, the ``int`` or ``str`` to convert. + + Raises a class-specific exception if a ``str`` is provided that + cannot be converted. + + Raises ``ValueError`` if the value is out of range. + + Returns an enumeration from the calling class corresponding to the + value, if one is defined, or an ``int`` otherwise. + """ + + if isinstance(value, str): + return cls.from_text(value) + cls._check_value(value) + return cls(value) + + @classmethod + def _maximum(cls): + raise NotImplementedError # pragma: no cover + + @classmethod + def _short_name(cls): + return cls.__name__.lower() + + @classmethod + def _prefix(cls) -> str: + return "" + + @classmethod + def _extra_from_text(cls, text: str) -> Any | None: # pylint: disable=W0613 + return None + + @classmethod + def _extra_to_text(cls, value, current_text): # pylint: disable=W0613 + return current_text + + @classmethod + def _unknown_exception_class(cls) -> Type[Exception]: + return ValueError diff --git a/netdeploy/lib/python3.11/site-packages/dns/exception.py b/netdeploy/lib/python3.11/site-packages/dns/exception.py new file mode 100644 index 0000000..c3d42ff --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/exception.py @@ -0,0 +1,169 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Common DNS Exceptions. + +Dnspython modules may also define their own exceptions, which will +always be subclasses of ``DNSException``. +""" + + +from typing import Set + + +class DNSException(Exception): + """Abstract base class shared by all dnspython exceptions. + + It supports two basic modes of operation: + + a) Old/compatible mode is used if ``__init__`` was called with + empty *kwargs*. In compatible mode all *args* are passed + to the standard Python Exception class as before and all *args* are + printed by the standard ``__str__`` implementation. Class variable + ``msg`` (or doc string if ``msg`` is ``None``) is returned from ``str()`` + if *args* is empty. + + b) New/parametrized mode is used if ``__init__`` was called with + non-empty *kwargs*. + In the new mode *args* must be empty and all kwargs must match + those set in class variable ``supp_kwargs``. All kwargs are stored inside + ``self.kwargs`` and used in a new ``__str__`` implementation to construct + a formatted message based on the ``fmt`` class variable, a ``string``. + + In the simplest case it is enough to override the ``supp_kwargs`` + and ``fmt`` class variables to get nice parametrized messages. + """ + + msg: str | None = None # non-parametrized message + supp_kwargs: Set[str] = set() # accepted parameters for _fmt_kwargs (sanity check) + fmt: str | None = None # message parametrized with results from _fmt_kwargs + + def __init__(self, *args, **kwargs): + self._check_params(*args, **kwargs) + if kwargs: + # This call to a virtual method from __init__ is ok in our usage + self.kwargs = self._check_kwargs(**kwargs) # lgtm[py/init-calls-subclass] + self.msg = str(self) + else: + self.kwargs = dict() # defined but empty for old mode exceptions + if self.msg is None: + # doc string is better implicit message than empty string + self.msg = self.__doc__ + if args: + super().__init__(*args) + else: + super().__init__(self.msg) + + def _check_params(self, *args, **kwargs): + """Old exceptions supported only args and not kwargs. + + For sanity we do not allow to mix old and new behavior.""" + if args or kwargs: + assert bool(args) != bool( + kwargs + ), "keyword arguments are mutually exclusive with positional args" + + def _check_kwargs(self, **kwargs): + if kwargs: + assert ( + set(kwargs.keys()) == self.supp_kwargs + ), f"following set of keyword args is required: {self.supp_kwargs}" + return kwargs + + def _fmt_kwargs(self, **kwargs): + """Format kwargs before printing them. + + Resulting dictionary has to have keys necessary for str.format call + on fmt class variable. + """ + fmtargs = {} + for kw, data in kwargs.items(): + if isinstance(data, list | set): + # convert list of to list of str() + fmtargs[kw] = list(map(str, data)) + if len(fmtargs[kw]) == 1: + # remove list brackets [] from single-item lists + fmtargs[kw] = fmtargs[kw].pop() + else: + fmtargs[kw] = data + return fmtargs + + def __str__(self): + if self.kwargs and self.fmt: + # provide custom message constructed from keyword arguments + fmtargs = self._fmt_kwargs(**self.kwargs) + return self.fmt.format(**fmtargs) + else: + # print *args directly in the same way as old DNSException + return super().__str__() + + +class FormError(DNSException): + """DNS message is malformed.""" + + +class SyntaxError(DNSException): + """Text input is malformed.""" + + +class UnexpectedEnd(SyntaxError): + """Text input ended unexpectedly.""" + + +class TooBig(DNSException): + """The DNS message is too big.""" + + +class Timeout(DNSException): + """The DNS operation timed out.""" + + supp_kwargs = {"timeout"} + fmt = "The DNS operation timed out after {timeout:.3f} seconds" + + # We do this as otherwise mypy complains about unexpected keyword argument + # idna_exception + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class UnsupportedAlgorithm(DNSException): + """The DNSSEC algorithm is not supported.""" + + +class AlgorithmKeyMismatch(UnsupportedAlgorithm): + """The DNSSEC algorithm is not supported for the given key type.""" + + +class ValidationFailure(DNSException): + """The DNSSEC signature is invalid.""" + + +class DeniedByPolicy(DNSException): + """Denied by DNSSEC policy.""" + + +class ExceptionWrapper: + def __init__(self, exception_class): + self.exception_class = exception_class + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if exc_type is not None and not isinstance(exc_val, self.exception_class): + raise self.exception_class(str(exc_val)) from exc_val + return False diff --git a/netdeploy/lib/python3.11/site-packages/dns/flags.py b/netdeploy/lib/python3.11/site-packages/dns/flags.py new file mode 100644 index 0000000..4c60be1 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/flags.py @@ -0,0 +1,123 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Message Flags.""" + +import enum +from typing import Any + +# Standard DNS flags + + +class Flag(enum.IntFlag): + #: Query Response + QR = 0x8000 + #: Authoritative Answer + AA = 0x0400 + #: Truncated Response + TC = 0x0200 + #: Recursion Desired + RD = 0x0100 + #: Recursion Available + RA = 0x0080 + #: Authentic Data + AD = 0x0020 + #: Checking Disabled + CD = 0x0010 + + +# EDNS flags + + +class EDNSFlag(enum.IntFlag): + #: DNSSEC answer OK + DO = 0x8000 + + +def _from_text(text: str, enum_class: Any) -> int: + flags = 0 + tokens = text.split() + for t in tokens: + flags |= enum_class[t.upper()] + return flags + + +def _to_text(flags: int, enum_class: Any) -> str: + text_flags = [] + for k, v in enum_class.__members__.items(): + if flags & v != 0: + text_flags.append(k) + return " ".join(text_flags) + + +def from_text(text: str) -> int: + """Convert a space-separated list of flag text values into a flags + value. + + Returns an ``int`` + """ + + return _from_text(text, Flag) + + +def to_text(flags: int) -> str: + """Convert a flags value into a space-separated list of flag text + values. + + Returns a ``str``. + """ + + return _to_text(flags, Flag) + + +def edns_from_text(text: str) -> int: + """Convert a space-separated list of EDNS flag text values into a EDNS + flags value. + + Returns an ``int`` + """ + + return _from_text(text, EDNSFlag) + + +def edns_to_text(flags: int) -> str: + """Convert an EDNS flags value into a space-separated list of EDNS flag + text values. + + Returns a ``str``. + """ + + return _to_text(flags, EDNSFlag) + + +### BEGIN generated Flag constants + +QR = Flag.QR +AA = Flag.AA +TC = Flag.TC +RD = Flag.RD +RA = Flag.RA +AD = Flag.AD +CD = Flag.CD + +### END generated Flag constants + +### BEGIN generated EDNSFlag constants + +DO = EDNSFlag.DO + +### END generated EDNSFlag constants diff --git a/netdeploy/lib/python3.11/site-packages/dns/grange.py b/netdeploy/lib/python3.11/site-packages/dns/grange.py new file mode 100644 index 0000000..8d366dc --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/grange.py @@ -0,0 +1,72 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2012-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS GENERATE range conversion.""" + +from typing import Tuple + +import dns.exception + + +def from_text(text: str) -> Tuple[int, int, int]: + """Convert the text form of a range in a ``$GENERATE`` statement to an + integer. + + *text*, a ``str``, the textual range in ``$GENERATE`` form. + + Returns a tuple of three ``int`` values ``(start, stop, step)``. + """ + + start = -1 + stop = -1 + step = 1 + cur = "" + state = 0 + # state 0 1 2 + # x - y / z + + if text and text[0] == "-": + raise dns.exception.SyntaxError("Start cannot be a negative number") + + for c in text: + if c == "-" and state == 0: + start = int(cur) + cur = "" + state = 1 + elif c == "/": + stop = int(cur) + cur = "" + state = 2 + elif c.isdigit(): + cur += c + else: + raise dns.exception.SyntaxError(f"Could not parse {c}") + + if state == 0: + raise dns.exception.SyntaxError("no stop value specified") + elif state == 1: + stop = int(cur) + else: + assert state == 2 + step = int(cur) + + assert step >= 1 + assert start >= 0 + if start > stop: + raise dns.exception.SyntaxError("start must be <= stop") + + return (start, stop, step) diff --git a/netdeploy/lib/python3.11/site-packages/dns/immutable.py b/netdeploy/lib/python3.11/site-packages/dns/immutable.py new file mode 100644 index 0000000..36b0362 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/immutable.py @@ -0,0 +1,68 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import collections.abc +from typing import Any, Callable + +from dns._immutable_ctx import immutable + + +@immutable +class Dict(collections.abc.Mapping): # lgtm[py/missing-equals] + def __init__( + self, + dictionary: Any, + no_copy: bool = False, + map_factory: Callable[[], collections.abc.MutableMapping] = dict, + ): + """Make an immutable dictionary from the specified dictionary. + + If *no_copy* is `True`, then *dictionary* will be wrapped instead + of copied. Only set this if you are sure there will be no external + references to the dictionary. + """ + if no_copy and isinstance(dictionary, collections.abc.MutableMapping): + self._odict = dictionary + else: + self._odict = map_factory() + self._odict.update(dictionary) + self._hash = None + + def __getitem__(self, key): + return self._odict.__getitem__(key) + + def __hash__(self): # pylint: disable=invalid-hash-returned + if self._hash is None: + h = 0 + for key in sorted(self._odict.keys()): + h ^= hash(key) + object.__setattr__(self, "_hash", h) + # this does return an int, but pylint doesn't figure that out + return self._hash + + def __len__(self): + return len(self._odict) + + def __iter__(self): + return iter(self._odict) + + +def constify(o: Any) -> Any: + """ + Convert mutable types to immutable types. + """ + if isinstance(o, bytearray): + return bytes(o) + if isinstance(o, tuple): + try: + hash(o) + return o + except Exception: + return tuple(constify(elt) for elt in o) + if isinstance(o, list): + return tuple(constify(elt) for elt in o) + if isinstance(o, dict): + cdict = dict() + for k, v in o.items(): + cdict[k] = constify(v) + return Dict(cdict, True) + return o diff --git a/netdeploy/lib/python3.11/site-packages/dns/inet.py b/netdeploy/lib/python3.11/site-packages/dns/inet.py new file mode 100644 index 0000000..765203b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/inet.py @@ -0,0 +1,195 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Generic Internet address helper functions.""" + +import socket +from typing import Any, Tuple + +import dns.ipv4 +import dns.ipv6 + +# We assume that AF_INET and AF_INET6 are always defined. We keep +# these here for the benefit of any old code (unlikely though that +# is!). +AF_INET = socket.AF_INET +AF_INET6 = socket.AF_INET6 + + +def inet_pton(family: int, text: str) -> bytes: + """Convert the textual form of a network address into its binary form. + + *family* is an ``int``, the address family. + + *text* is a ``str``, the textual address. + + Raises ``NotImplementedError`` if the address family specified is not + implemented. + + Returns a ``bytes``. + """ + + if family == AF_INET: + return dns.ipv4.inet_aton(text) + elif family == AF_INET6: + return dns.ipv6.inet_aton(text, True) + else: + raise NotImplementedError + + +def inet_ntop(family: int, address: bytes) -> str: + """Convert the binary form of a network address into its textual form. + + *family* is an ``int``, the address family. + + *address* is a ``bytes``, the network address in binary form. + + Raises ``NotImplementedError`` if the address family specified is not + implemented. + + Returns a ``str``. + """ + + if family == AF_INET: + return dns.ipv4.inet_ntoa(address) + elif family == AF_INET6: + return dns.ipv6.inet_ntoa(address) + else: + raise NotImplementedError + + +def af_for_address(text: str) -> int: + """Determine the address family of a textual-form network address. + + *text*, a ``str``, the textual address. + + Raises ``ValueError`` if the address family cannot be determined + from the input. + + Returns an ``int``. + """ + + try: + dns.ipv4.inet_aton(text) + return AF_INET + except Exception: + try: + dns.ipv6.inet_aton(text, True) + return AF_INET6 + except Exception: + raise ValueError + + +def is_multicast(text: str) -> bool: + """Is the textual-form network address a multicast address? + + *text*, a ``str``, the textual address. + + Raises ``ValueError`` if the address family cannot be determined + from the input. + + Returns a ``bool``. + """ + + try: + first = dns.ipv4.inet_aton(text)[0] + return first >= 224 and first <= 239 + except Exception: + try: + first = dns.ipv6.inet_aton(text, True)[0] + return first == 255 + except Exception: + raise ValueError + + +def is_address(text: str) -> bool: + """Is the specified string an IPv4 or IPv6 address? + + *text*, a ``str``, the textual address. + + Returns a ``bool``. + """ + + try: + dns.ipv4.inet_aton(text) + return True + except Exception: + try: + dns.ipv6.inet_aton(text, True) + return True + except Exception: + return False + + +def low_level_address_tuple(high_tuple: Tuple[str, int], af: int | None = None) -> Any: + """Given a "high-level" address tuple, i.e. + an (address, port) return the appropriate "low-level" address tuple + suitable for use in socket calls. + + If an *af* other than ``None`` is provided, it is assumed the + address in the high-level tuple is valid and has that af. If af + is ``None``, then af_for_address will be called. + """ + address, port = high_tuple + if af is None: + af = af_for_address(address) + if af == AF_INET: + return (address, port) + elif af == AF_INET6: + i = address.find("%") + if i < 0: + # no scope, shortcut! + return (address, port, 0, 0) + # try to avoid getaddrinfo() + addrpart = address[:i] + scope = address[i + 1 :] + if scope.isdigit(): + return (addrpart, port, 0, int(scope)) + try: + return (addrpart, port, 0, socket.if_nametoindex(scope)) + except AttributeError: # pragma: no cover (we can't really test this) + ai_flags = socket.AI_NUMERICHOST + ((*_, tup), *_) = socket.getaddrinfo(address, port, flags=ai_flags) + return tup + else: + raise NotImplementedError(f"unknown address family {af}") + + +def any_for_af(af): + """Return the 'any' address for the specified address family.""" + if af == socket.AF_INET: + return "0.0.0.0" + elif af == socket.AF_INET6: + return "::" + raise NotImplementedError(f"unknown address family {af}") + + +def canonicalize(text: str) -> str: + """Verify that *address* is a valid text form IPv4 or IPv6 address and return its + canonical text form. IPv6 addresses with scopes are rejected. + + *text*, a ``str``, the address in textual form. + + Raises ``ValueError`` if the text is not valid. + """ + try: + return dns.ipv6.canonicalize(text) + except Exception: + try: + return dns.ipv4.canonicalize(text) + except Exception: + raise ValueError diff --git a/netdeploy/lib/python3.11/site-packages/dns/ipv4.py b/netdeploy/lib/python3.11/site-packages/dns/ipv4.py new file mode 100644 index 0000000..a7161bc --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/ipv4.py @@ -0,0 +1,76 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""IPv4 helper functions.""" + +import struct + +import dns.exception + + +def inet_ntoa(address: bytes) -> str: + """Convert an IPv4 address in binary form to text form. + + *address*, a ``bytes``, the IPv4 address in binary form. + + Returns a ``str``. + """ + + if len(address) != 4: + raise dns.exception.SyntaxError + return f"{address[0]}.{address[1]}.{address[2]}.{address[3]}" + + +def inet_aton(text: str | bytes) -> bytes: + """Convert an IPv4 address in text form to binary form. + + *text*, a ``str`` or ``bytes``, the IPv4 address in textual form. + + Returns a ``bytes``. + """ + + if not isinstance(text, bytes): + btext = text.encode() + else: + btext = text + parts = btext.split(b".") + if len(parts) != 4: + raise dns.exception.SyntaxError + for part in parts: + if not part.isdigit(): + raise dns.exception.SyntaxError + if len(part) > 1 and part[0] == ord("0"): + # No leading zeros + raise dns.exception.SyntaxError + try: + b = [int(part) for part in parts] + return struct.pack("BBBB", *b) + except Exception: + raise dns.exception.SyntaxError + + +def canonicalize(text: str | bytes) -> str: + """Verify that *address* is a valid text form IPv4 address and return its + canonical text form. + + *text*, a ``str`` or ``bytes``, the IPv4 address in textual form. + + Raises ``dns.exception.SyntaxError`` if the text is not valid. + """ + # Note that inet_aton() only accepts canonial form, but we still run through + # inet_ntoa() to ensure the output is a str. + return inet_ntoa(inet_aton(text)) diff --git a/netdeploy/lib/python3.11/site-packages/dns/ipv6.py b/netdeploy/lib/python3.11/site-packages/dns/ipv6.py new file mode 100644 index 0000000..eaa0f6c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/ipv6.py @@ -0,0 +1,217 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""IPv6 helper functions.""" + +import binascii +import re +from typing import List + +import dns.exception +import dns.ipv4 + +_leading_zero = re.compile(r"0+([0-9a-f]+)") + + +def inet_ntoa(address: bytes) -> str: + """Convert an IPv6 address in binary form to text form. + + *address*, a ``bytes``, the IPv6 address in binary form. + + Raises ``ValueError`` if the address isn't 16 bytes long. + Returns a ``str``. + """ + + if len(address) != 16: + raise ValueError("IPv6 addresses are 16 bytes long") + hex = binascii.hexlify(address) + chunks = [] + i = 0 + l = len(hex) + while i < l: + chunk = hex[i : i + 4].decode() + # strip leading zeros. we do this with an re instead of + # with lstrip() because lstrip() didn't support chars until + # python 2.2.2 + m = _leading_zero.match(chunk) + if m is not None: + chunk = m.group(1) + chunks.append(chunk) + i += 4 + # + # Compress the longest subsequence of 0-value chunks to :: + # + best_start = 0 + best_len = 0 + start = -1 + last_was_zero = False + for i in range(8): + if chunks[i] != "0": + if last_was_zero: + end = i + current_len = end - start + if current_len > best_len: + best_start = start + best_len = current_len + last_was_zero = False + elif not last_was_zero: + start = i + last_was_zero = True + if last_was_zero: + end = 8 + current_len = end - start + if current_len > best_len: + best_start = start + best_len = current_len + if best_len > 1: + if best_start == 0 and (best_len == 6 or best_len == 5 and chunks[5] == "ffff"): + # We have an embedded IPv4 address + if best_len == 6: + prefix = "::" + else: + prefix = "::ffff:" + thex = prefix + dns.ipv4.inet_ntoa(address[12:]) + else: + thex = ( + ":".join(chunks[:best_start]) + + "::" + + ":".join(chunks[best_start + best_len :]) + ) + else: + thex = ":".join(chunks) + return thex + + +_v4_ending = re.compile(rb"(.*):(\d+\.\d+\.\d+\.\d+)$") +_colon_colon_start = re.compile(rb"::.*") +_colon_colon_end = re.compile(rb".*::$") + + +def inet_aton(text: str | bytes, ignore_scope: bool = False) -> bytes: + """Convert an IPv6 address in text form to binary form. + + *text*, a ``str`` or ``bytes``, the IPv6 address in textual form. + + *ignore_scope*, a ``bool``. If ``True``, a scope will be ignored. + If ``False``, the default, it is an error for a scope to be present. + + Returns a ``bytes``. + """ + + # + # Our aim here is not something fast; we just want something that works. + # + if not isinstance(text, bytes): + btext = text.encode() + else: + btext = text + + if ignore_scope: + parts = btext.split(b"%") + l = len(parts) + if l == 2: + btext = parts[0] + elif l > 2: + raise dns.exception.SyntaxError + + if btext == b"": + raise dns.exception.SyntaxError + elif btext.endswith(b":") and not btext.endswith(b"::"): + raise dns.exception.SyntaxError + elif btext.startswith(b":") and not btext.startswith(b"::"): + raise dns.exception.SyntaxError + elif btext == b"::": + btext = b"0::" + # + # Get rid of the icky dot-quad syntax if we have it. + # + m = _v4_ending.match(btext) + if m is not None: + b = dns.ipv4.inet_aton(m.group(2)) + btext = ( + f"{m.group(1).decode()}:{b[0]:02x}{b[1]:02x}:{b[2]:02x}{b[3]:02x}" + ).encode() + # + # Try to turn '::' into ':'; if no match try to + # turn '::' into ':' + # + m = _colon_colon_start.match(btext) + if m is not None: + btext = btext[1:] + else: + m = _colon_colon_end.match(btext) + if m is not None: + btext = btext[:-1] + # + # Now canonicalize into 8 chunks of 4 hex digits each + # + chunks = btext.split(b":") + l = len(chunks) + if l > 8: + raise dns.exception.SyntaxError + seen_empty = False + canonical: List[bytes] = [] + for c in chunks: + if c == b"": + if seen_empty: + raise dns.exception.SyntaxError + seen_empty = True + for _ in range(0, 8 - l + 1): + canonical.append(b"0000") + else: + lc = len(c) + if lc > 4: + raise dns.exception.SyntaxError + if lc != 4: + c = (b"0" * (4 - lc)) + c + canonical.append(c) + if l < 8 and not seen_empty: + raise dns.exception.SyntaxError + btext = b"".join(canonical) + + # + # Finally we can go to binary. + # + try: + return binascii.unhexlify(btext) + except (binascii.Error, TypeError): + raise dns.exception.SyntaxError + + +_mapped_prefix = b"\x00" * 10 + b"\xff\xff" + + +def is_mapped(address: bytes) -> bool: + """Is the specified address a mapped IPv4 address? + + *address*, a ``bytes`` is an IPv6 address in binary form. + + Returns a ``bool``. + """ + + return address.startswith(_mapped_prefix) + + +def canonicalize(text: str | bytes) -> str: + """Verify that *address* is a valid text form IPv6 address and return its + canonical text form. Addresses with scopes are rejected. + + *text*, a ``str`` or ``bytes``, the IPv6 address in textual form. + + Raises ``dns.exception.SyntaxError`` if the text is not valid. + """ + return inet_ntoa(inet_aton(text)) diff --git a/netdeploy/lib/python3.11/site-packages/dns/message.py b/netdeploy/lib/python3.11/site-packages/dns/message.py new file mode 100644 index 0000000..bbfccfc --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/message.py @@ -0,0 +1,1954 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Messages""" + +import contextlib +import enum +import io +import time +from typing import Any, Dict, List, Tuple, cast + +import dns.edns +import dns.entropy +import dns.enum +import dns.exception +import dns.flags +import dns.name +import dns.opcode +import dns.rcode +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.rdtypes.ANY.OPT +import dns.rdtypes.ANY.SOA +import dns.rdtypes.ANY.TSIG +import dns.renderer +import dns.rrset +import dns.tokenizer +import dns.tsig +import dns.ttl +import dns.wire + + +class ShortHeader(dns.exception.FormError): + """The DNS packet passed to from_wire() is too short.""" + + +class TrailingJunk(dns.exception.FormError): + """The DNS packet passed to from_wire() has extra junk at the end of it.""" + + +class UnknownHeaderField(dns.exception.DNSException): + """The header field name was not recognized when converting from text + into a message.""" + + +class BadEDNS(dns.exception.FormError): + """An OPT record occurred somewhere other than + the additional data section.""" + + +class BadTSIG(dns.exception.FormError): + """A TSIG record occurred somewhere other than the end of + the additional data section.""" + + +class UnknownTSIGKey(dns.exception.DNSException): + """A TSIG with an unknown key was received.""" + + +class Truncated(dns.exception.DNSException): + """The truncated flag is set.""" + + supp_kwargs = {"message"} + + # We do this as otherwise mypy complains about unexpected keyword argument + # idna_exception + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def message(self): + """As much of the message as could be processed. + + Returns a ``dns.message.Message``. + """ + return self.kwargs["message"] + + +class NotQueryResponse(dns.exception.DNSException): + """Message is not a response to a query.""" + + +class ChainTooLong(dns.exception.DNSException): + """The CNAME chain is too long.""" + + +class AnswerForNXDOMAIN(dns.exception.DNSException): + """The rcode is NXDOMAIN but an answer was found.""" + + +class NoPreviousName(dns.exception.SyntaxError): + """No previous name was known.""" + + +class MessageSection(dns.enum.IntEnum): + """Message sections""" + + QUESTION = 0 + ANSWER = 1 + AUTHORITY = 2 + ADDITIONAL = 3 + + @classmethod + def _maximum(cls): + return 3 + + +class MessageError: + def __init__(self, exception: Exception, offset: int): + self.exception = exception + self.offset = offset + + +DEFAULT_EDNS_PAYLOAD = 1232 +MAX_CHAIN = 16 + +IndexKeyType = Tuple[ + int, + dns.name.Name, + dns.rdataclass.RdataClass, + dns.rdatatype.RdataType, + dns.rdatatype.RdataType | None, + dns.rdataclass.RdataClass | None, +] +IndexType = Dict[IndexKeyType, dns.rrset.RRset] +SectionType = int | str | List[dns.rrset.RRset] + + +class Message: + """A DNS message.""" + + _section_enum = MessageSection + + def __init__(self, id: int | None = None): + if id is None: + self.id = dns.entropy.random_16() + else: + self.id = id + self.flags = 0 + self.sections: List[List[dns.rrset.RRset]] = [[], [], [], []] + self.opt: dns.rrset.RRset | None = None + self.request_payload = 0 + self.pad = 0 + self.keyring: Any = None + self.tsig: dns.rrset.RRset | None = None + self.want_tsig_sign = False + self.request_mac = b"" + self.xfr = False + self.origin: dns.name.Name | None = None + self.tsig_ctx: Any | None = None + self.index: IndexType = {} + self.errors: List[MessageError] = [] + self.time = 0.0 + self.wire: bytes | None = None + + @property + def question(self) -> List[dns.rrset.RRset]: + """The question section.""" + return self.sections[0] + + @question.setter + def question(self, v): + self.sections[0] = v + + @property + def answer(self) -> List[dns.rrset.RRset]: + """The answer section.""" + return self.sections[1] + + @answer.setter + def answer(self, v): + self.sections[1] = v + + @property + def authority(self) -> List[dns.rrset.RRset]: + """The authority section.""" + return self.sections[2] + + @authority.setter + def authority(self, v): + self.sections[2] = v + + @property + def additional(self) -> List[dns.rrset.RRset]: + """The additional data section.""" + return self.sections[3] + + @additional.setter + def additional(self, v): + self.sections[3] = v + + def __repr__(self): + return "" + + def __str__(self): + return self.to_text() + + def to_text( + self, + origin: dns.name.Name | None = None, + relativize: bool = True, + **kw: Dict[str, Any], + ) -> str: + """Convert the message to text. + + The *origin*, *relativize*, and any other keyword + arguments are passed to the RRset ``to_wire()`` method. + + Returns a ``str``. + """ + + s = io.StringIO() + s.write(f"id {self.id}\n") + s.write(f"opcode {dns.opcode.to_text(self.opcode())}\n") + s.write(f"rcode {dns.rcode.to_text(self.rcode())}\n") + s.write(f"flags {dns.flags.to_text(self.flags)}\n") + if self.edns >= 0: + s.write(f"edns {self.edns}\n") + if self.ednsflags != 0: + s.write(f"eflags {dns.flags.edns_to_text(self.ednsflags)}\n") + s.write(f"payload {self.payload}\n") + for opt in self.options: + s.write(f"option {opt.to_text()}\n") + for name, which in self._section_enum.__members__.items(): + s.write(f";{name}\n") + for rrset in self.section_from_number(which): + s.write(rrset.to_text(origin, relativize, **kw)) + s.write("\n") + if self.tsig is not None: + s.write(self.tsig.to_text(origin, relativize, **kw)) + s.write("\n") + # + # We strip off the final \n so the caller can print the result without + # doing weird things to get around eccentricities in Python print + # formatting + # + return s.getvalue()[:-1] + + def __eq__(self, other): + """Two messages are equal if they have the same content in the + header, question, answer, and authority sections. + + Returns a ``bool``. + """ + + if not isinstance(other, Message): + return False + if self.id != other.id: + return False + if self.flags != other.flags: + return False + for i, section in enumerate(self.sections): + other_section = other.sections[i] + for n in section: + if n not in other_section: + return False + for n in other_section: + if n not in section: + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def is_response(self, other: "Message") -> bool: + """Is *other*, also a ``dns.message.Message``, a response to this + message? + + Returns a ``bool``. + """ + + if ( + other.flags & dns.flags.QR == 0 + or self.id != other.id + or dns.opcode.from_flags(self.flags) != dns.opcode.from_flags(other.flags) + ): + return False + if other.rcode() in { + dns.rcode.FORMERR, + dns.rcode.SERVFAIL, + dns.rcode.NOTIMP, + dns.rcode.REFUSED, + }: + # We don't check the question section in these cases if + # the other question section is empty, even though they + # still really ought to have a question section. + if len(other.question) == 0: + return True + if dns.opcode.is_update(self.flags): + # This is assuming the "sender doesn't include anything + # from the update", but we don't care to check the other + # case, which is that all the sections are returned and + # identical. + return True + for n in self.question: + if n not in other.question: + return False + for n in other.question: + if n not in self.question: + return False + return True + + def section_number(self, section: List[dns.rrset.RRset]) -> int: + """Return the "section number" of the specified section for use + in indexing. + + *section* is one of the section attributes of this message. + + Raises ``ValueError`` if the section isn't known. + + Returns an ``int``. + """ + + for i, our_section in enumerate(self.sections): + if section is our_section: + return self._section_enum(i) + raise ValueError("unknown section") + + def section_from_number(self, number: int) -> List[dns.rrset.RRset]: + """Return the section list associated with the specified section + number. + + *number* is a section number `int` or the text form of a section + name. + + Raises ``ValueError`` if the section isn't known. + + Returns a ``list``. + """ + + section = self._section_enum.make(number) + return self.sections[section] + + def find_rrset( + self, + section: SectionType, + name: dns.name.Name, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + deleting: dns.rdataclass.RdataClass | None = None, + create: bool = False, + force_unique: bool = False, + idna_codec: dns.name.IDNACodec | None = None, + ) -> dns.rrset.RRset: + """Find the RRset with the given attributes in the specified section. + + *section*, an ``int`` section number, a ``str`` section name, or one of + the section attributes of this message. This specifies the + the section of the message to search. For example:: + + my_message.find_rrset(my_message.answer, name, rdclass, rdtype) + my_message.find_rrset(dns.message.ANSWER, name, rdclass, rdtype) + my_message.find_rrset("ANSWER", name, rdclass, rdtype) + + *name*, a ``dns.name.Name`` or ``str``, the name of the RRset. + + *rdclass*, an ``int`` or ``str``, the class of the RRset. + + *rdtype*, an ``int`` or ``str``, the type of the RRset. + + *covers*, an ``int`` or ``str``, the covers value of the RRset. + The default is ``dns.rdatatype.NONE``. + + *deleting*, an ``int``, ``str``, or ``None``, the deleting value of the + RRset. The default is ``None``. + + *create*, a ``bool``. If ``True``, create the RRset if it is not found. + The created RRset is appended to *section*. + + *force_unique*, a ``bool``. If ``True`` and *create* is also ``True``, + create a new RRset regardless of whether a matching RRset exists + already. The default is ``False``. This is useful when creating + DDNS Update messages, as order matters for them. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + Raises ``KeyError`` if the RRset was not found and create was + ``False``. + + Returns a ``dns.rrset.RRset object``. + """ + + if isinstance(section, int): + section_number = section + section = self.section_from_number(section_number) + elif isinstance(section, str): + section_number = self._section_enum.from_text(section) + section = self.section_from_number(section_number) + else: + section_number = self.section_number(section) + if isinstance(name, str): + name = dns.name.from_text(name, idna_codec=idna_codec) + rdtype = dns.rdatatype.RdataType.make(rdtype) + rdclass = dns.rdataclass.RdataClass.make(rdclass) + covers = dns.rdatatype.RdataType.make(covers) + if deleting is not None: + deleting = dns.rdataclass.RdataClass.make(deleting) + key = (section_number, name, rdclass, rdtype, covers, deleting) + if not force_unique: + if self.index is not None: + rrset = self.index.get(key) + if rrset is not None: + return rrset + else: + for rrset in section: + if rrset.full_match(name, rdclass, rdtype, covers, deleting): + return rrset + if not create: + raise KeyError + rrset = dns.rrset.RRset(name, rdclass, rdtype, covers, deleting) + section.append(rrset) + if self.index is not None: + self.index[key] = rrset + return rrset + + def get_rrset( + self, + section: SectionType, + name: dns.name.Name, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + deleting: dns.rdataclass.RdataClass | None = None, + create: bool = False, + force_unique: bool = False, + idna_codec: dns.name.IDNACodec | None = None, + ) -> dns.rrset.RRset | None: + """Get the RRset with the given attributes in the specified section. + + If the RRset is not found, None is returned. + + *section*, an ``int`` section number, a ``str`` section name, or one of + the section attributes of this message. This specifies the + the section of the message to search. For example:: + + my_message.get_rrset(my_message.answer, name, rdclass, rdtype) + my_message.get_rrset(dns.message.ANSWER, name, rdclass, rdtype) + my_message.get_rrset("ANSWER", name, rdclass, rdtype) + + *name*, a ``dns.name.Name`` or ``str``, the name of the RRset. + + *rdclass*, an ``int`` or ``str``, the class of the RRset. + + *rdtype*, an ``int`` or ``str``, the type of the RRset. + + *covers*, an ``int`` or ``str``, the covers value of the RRset. + The default is ``dns.rdatatype.NONE``. + + *deleting*, an ``int``, ``str``, or ``None``, the deleting value of the + RRset. The default is ``None``. + + *create*, a ``bool``. If ``True``, create the RRset if it is not found. + The created RRset is appended to *section*. + + *force_unique*, a ``bool``. If ``True`` and *create* is also ``True``, + create a new RRset regardless of whether a matching RRset exists + already. The default is ``False``. This is useful when creating + DDNS Update messages, as order matters for them. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + Returns a ``dns.rrset.RRset object`` or ``None``. + """ + + try: + rrset = self.find_rrset( + section, + name, + rdclass, + rdtype, + covers, + deleting, + create, + force_unique, + idna_codec, + ) + except KeyError: + rrset = None + return rrset + + def section_count(self, section: SectionType) -> int: + """Returns the number of records in the specified section. + + *section*, an ``int`` section number, a ``str`` section name, or one of + the section attributes of this message. This specifies the + the section of the message to count. For example:: + + my_message.section_count(my_message.answer) + my_message.section_count(dns.message.ANSWER) + my_message.section_count("ANSWER") + """ + + if isinstance(section, int): + section_number = section + section = self.section_from_number(section_number) + elif isinstance(section, str): + section_number = self._section_enum.from_text(section) + section = self.section_from_number(section_number) + else: + section_number = self.section_number(section) + count = sum(max(1, len(rrs)) for rrs in section) + if section_number == MessageSection.ADDITIONAL: + if self.opt is not None: + count += 1 + if self.tsig is not None: + count += 1 + return count + + def _compute_opt_reserve(self) -> int: + """Compute the size required for the OPT RR, padding excluded""" + if not self.opt: + return 0 + # 1 byte for the root name, 10 for the standard RR fields + size = 11 + # This would be more efficient if options had a size() method, but we won't + # worry about that for now. We also don't worry if there is an existing padding + # option, as it is unlikely and probably harmless, as the worst case is that we + # may add another, and this seems to be legal. + opt_rdata = cast(dns.rdtypes.ANY.OPT.OPT, self.opt[0]) + for option in opt_rdata.options: + wire = option.to_wire() + # We add 4 here to account for the option type and length + size += len(wire) + 4 + if self.pad: + # Padding will be added, so again add the option type and length. + size += 4 + return size + + def _compute_tsig_reserve(self) -> int: + """Compute the size required for the TSIG RR""" + # This would be more efficient if TSIGs had a size method, but we won't + # worry about for now. Also, we can't really cope with the potential + # compressibility of the TSIG owner name, so we estimate with the uncompressed + # size. We will disable compression when TSIG and padding are both is active + # so that the padding comes out right. + if not self.tsig: + return 0 + f = io.BytesIO() + self.tsig.to_wire(f) + return len(f.getvalue()) + + def to_wire( + self, + origin: dns.name.Name | None = None, + max_size: int = 0, + multi: bool = False, + tsig_ctx: Any | None = None, + prepend_length: bool = False, + prefer_truncation: bool = False, + **kw: Dict[str, Any], + ) -> bytes: + """Return a string containing the message in DNS compressed wire + format. + + Additional keyword arguments are passed to the RRset ``to_wire()`` + method. + + *origin*, a ``dns.name.Name`` or ``None``, the origin to be appended + to any relative names. If ``None``, and the message has an origin + attribute that is not ``None``, then it will be used. + + *max_size*, an ``int``, the maximum size of the wire format + output; default is 0, which means "the message's request + payload, if nonzero, or 65535". + + *multi*, a ``bool``, should be set to ``True`` if this message is + part of a multiple message sequence. + + *tsig_ctx*, a ``dns.tsig.HMACTSig`` or ``dns.tsig.GSSTSig`` object, the + ongoing TSIG context, used when signing zone transfers. + + *prepend_length*, a ``bool``, should be set to ``True`` if the caller + wants the message length prepended to the message itself. This is + useful for messages sent over TCP, TLS (DoT), or QUIC (DoQ). + + *prefer_truncation*, a ``bool``, should be set to ``True`` if the caller + wants the message to be truncated if it would otherwise exceed the + maximum length. If the truncation occurs before the additional section, + the TC bit will be set. + + Raises ``dns.exception.TooBig`` if *max_size* was exceeded. + + Returns a ``bytes``. + """ + + if origin is None and self.origin is not None: + origin = self.origin + if max_size == 0: + if self.request_payload != 0: + max_size = self.request_payload + else: + max_size = 65535 + if max_size < 512: + max_size = 512 + elif max_size > 65535: + max_size = 65535 + r = dns.renderer.Renderer(self.id, self.flags, max_size, origin) + opt_reserve = self._compute_opt_reserve() + r.reserve(opt_reserve) + tsig_reserve = self._compute_tsig_reserve() + r.reserve(tsig_reserve) + try: + for rrset in self.question: + r.add_question(rrset.name, rrset.rdtype, rrset.rdclass) + for rrset in self.answer: + r.add_rrset(dns.renderer.ANSWER, rrset, **kw) + for rrset in self.authority: + r.add_rrset(dns.renderer.AUTHORITY, rrset, **kw) + for rrset in self.additional: + r.add_rrset(dns.renderer.ADDITIONAL, rrset, **kw) + except dns.exception.TooBig: + if prefer_truncation: + if r.section < dns.renderer.ADDITIONAL: + r.flags |= dns.flags.TC + else: + raise + r.release_reserved() + if self.opt is not None: + r.add_opt(self.opt, self.pad, opt_reserve, tsig_reserve) + r.write_header() + if self.tsig is not None: + if self.want_tsig_sign: + (new_tsig, ctx) = dns.tsig.sign( + r.get_wire(), + self.keyring, + self.tsig[0], + int(time.time()), + self.request_mac, + tsig_ctx, + multi, + ) + self.tsig.clear() + self.tsig.add(new_tsig) + if multi: + self.tsig_ctx = ctx + r.add_rrset(dns.renderer.ADDITIONAL, self.tsig) + r.write_header() + wire = r.get_wire() + self.wire = wire + if prepend_length: + wire = len(wire).to_bytes(2, "big") + wire + return wire + + @staticmethod + def _make_tsig( + keyname, algorithm, time_signed, fudge, mac, original_id, error, other + ): + tsig = dns.rdtypes.ANY.TSIG.TSIG( + dns.rdataclass.ANY, + dns.rdatatype.TSIG, + algorithm, + time_signed, + fudge, + mac, + original_id, + error, + other, + ) + return dns.rrset.from_rdata(keyname, 0, tsig) + + def use_tsig( + self, + keyring: Any, + keyname: dns.name.Name | str | None = None, + fudge: int = 300, + original_id: int | None = None, + tsig_error: int = 0, + other_data: bytes = b"", + algorithm: dns.name.Name | str = dns.tsig.default_algorithm, + ) -> None: + """When sending, a TSIG signature using the specified key + should be added. + + *keyring*, a ``dict``, ``callable`` or ``dns.tsig.Key``, is either + the TSIG keyring or key to use. + + The format of a keyring dict is a mapping from TSIG key name, as + ``dns.name.Name`` to ``dns.tsig.Key`` or a TSIG secret, a ``bytes``. + If a ``dict`` *keyring* is specified but a *keyname* is not, the key + used will be the first key in the *keyring*. Note that the order of + keys in a dictionary is not defined, so applications should supply a + keyname when a ``dict`` keyring is used, unless they know the keyring + contains only one key. If a ``callable`` keyring is specified, the + callable will be called with the message and the keyname, and is + expected to return a key. + + *keyname*, a ``dns.name.Name``, ``str`` or ``None``, the name of + this TSIG key to use; defaults to ``None``. If *keyring* is a + ``dict``, the key must be defined in it. If *keyring* is a + ``dns.tsig.Key``, this is ignored. + + *fudge*, an ``int``, the TSIG time fudge. + + *original_id*, an ``int``, the TSIG original id. If ``None``, + the message's id is used. + + *tsig_error*, an ``int``, the TSIG error code. + + *other_data*, a ``bytes``, the TSIG other data. + + *algorithm*, a ``dns.name.Name`` or ``str``, the TSIG algorithm to use. This is + only used if *keyring* is a ``dict``, and the key entry is a ``bytes``. + """ + + if isinstance(keyring, dns.tsig.Key): + key = keyring + keyname = key.name + elif callable(keyring): + key = keyring(self, keyname) + else: + if isinstance(keyname, str): + keyname = dns.name.from_text(keyname) + if keyname is None: + keyname = next(iter(keyring)) + key = keyring[keyname] + if isinstance(key, bytes): + key = dns.tsig.Key(keyname, key, algorithm) + self.keyring = key + if original_id is None: + original_id = self.id + self.tsig = self._make_tsig( + keyname, + self.keyring.algorithm, + 0, + fudge, + b"\x00" * dns.tsig.mac_sizes[self.keyring.algorithm], + original_id, + tsig_error, + other_data, + ) + self.want_tsig_sign = True + + @property + def keyname(self) -> dns.name.Name | None: + if self.tsig: + return self.tsig.name + else: + return None + + @property + def keyalgorithm(self) -> dns.name.Name | None: + if self.tsig: + rdata = cast(dns.rdtypes.ANY.TSIG.TSIG, self.tsig[0]) + return rdata.algorithm + else: + return None + + @property + def mac(self) -> bytes | None: + if self.tsig: + rdata = cast(dns.rdtypes.ANY.TSIG.TSIG, self.tsig[0]) + return rdata.mac + else: + return None + + @property + def tsig_error(self) -> int | None: + if self.tsig: + rdata = cast(dns.rdtypes.ANY.TSIG.TSIG, self.tsig[0]) + return rdata.error + else: + return None + + @property + def had_tsig(self) -> bool: + return bool(self.tsig) + + @staticmethod + def _make_opt(flags=0, payload=DEFAULT_EDNS_PAYLOAD, options=None): + opt = dns.rdtypes.ANY.OPT.OPT(payload, dns.rdatatype.OPT, options or ()) + return dns.rrset.from_rdata(dns.name.root, int(flags), opt) + + def use_edns( + self, + edns: int | bool | None = 0, + ednsflags: int = 0, + payload: int = DEFAULT_EDNS_PAYLOAD, + request_payload: int | None = None, + options: List[dns.edns.Option] | None = None, + pad: int = 0, + ) -> None: + """Configure EDNS behavior. + + *edns*, an ``int``, is the EDNS level to use. Specifying ``None``, ``False``, + or ``-1`` means "do not use EDNS", and in this case the other parameters are + ignored. Specifying ``True`` is equivalent to specifying 0, i.e. "use EDNS0". + + *ednsflags*, an ``int``, the EDNS flag values. + + *payload*, an ``int``, is the EDNS sender's payload field, which is the maximum + size of UDP datagram the sender can handle. I.e. how big a response to this + message can be. + + *request_payload*, an ``int``, is the EDNS payload size to use when sending this + message. If not specified, defaults to the value of *payload*. + + *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS options. + + *pad*, a non-negative ``int``. If 0, the default, do not pad; otherwise add + padding bytes to make the message size a multiple of *pad*. Note that if + padding is non-zero, an EDNS PADDING option will always be added to the + message. + """ + + if edns is None or edns is False: + edns = -1 + elif edns is True: + edns = 0 + if edns < 0: + self.opt = None + self.request_payload = 0 + else: + # make sure the EDNS version in ednsflags agrees with edns + ednsflags &= 0xFF00FFFF + ednsflags |= edns << 16 + if options is None: + options = [] + self.opt = self._make_opt(ednsflags, payload, options) + if request_payload is None: + request_payload = payload + self.request_payload = request_payload + if pad < 0: + raise ValueError("pad must be non-negative") + self.pad = pad + + @property + def edns(self) -> int: + if self.opt: + return (self.ednsflags & 0xFF0000) >> 16 + else: + return -1 + + @property + def ednsflags(self) -> int: + if self.opt: + return self.opt.ttl + else: + return 0 + + @ednsflags.setter + def ednsflags(self, v): + if self.opt: + self.opt.ttl = v + elif v: + self.opt = self._make_opt(v) + + @property + def payload(self) -> int: + if self.opt: + rdata = cast(dns.rdtypes.ANY.OPT.OPT, self.opt[0]) + return rdata.payload + else: + return 0 + + @property + def options(self) -> Tuple: + if self.opt: + rdata = cast(dns.rdtypes.ANY.OPT.OPT, self.opt[0]) + return rdata.options + else: + return () + + def want_dnssec(self, wanted: bool = True) -> None: + """Enable or disable 'DNSSEC desired' flag in requests. + + *wanted*, a ``bool``. If ``True``, then DNSSEC data is + desired in the response, EDNS is enabled if required, and then + the DO bit is set. If ``False``, the DO bit is cleared if + EDNS is enabled. + """ + + if wanted: + self.ednsflags |= dns.flags.DO + elif self.opt: + self.ednsflags &= ~int(dns.flags.DO) + + def rcode(self) -> dns.rcode.Rcode: + """Return the rcode. + + Returns a ``dns.rcode.Rcode``. + """ + return dns.rcode.from_flags(int(self.flags), int(self.ednsflags)) + + def set_rcode(self, rcode: dns.rcode.Rcode) -> None: + """Set the rcode. + + *rcode*, a ``dns.rcode.Rcode``, is the rcode to set. + """ + (value, evalue) = dns.rcode.to_flags(rcode) + self.flags &= 0xFFF0 + self.flags |= value + self.ednsflags &= 0x00FFFFFF + self.ednsflags |= evalue + + def opcode(self) -> dns.opcode.Opcode: + """Return the opcode. + + Returns a ``dns.opcode.Opcode``. + """ + return dns.opcode.from_flags(int(self.flags)) + + def set_opcode(self, opcode: dns.opcode.Opcode) -> None: + """Set the opcode. + + *opcode*, a ``dns.opcode.Opcode``, is the opcode to set. + """ + self.flags &= 0x87FF + self.flags |= dns.opcode.to_flags(opcode) + + def get_options(self, otype: dns.edns.OptionType) -> List[dns.edns.Option]: + """Return the list of options of the specified type.""" + return [option for option in self.options if option.otype == otype] + + def extended_errors(self) -> List[dns.edns.EDEOption]: + """Return the list of Extended DNS Error (EDE) options in the message""" + return cast(List[dns.edns.EDEOption], self.get_options(dns.edns.OptionType.EDE)) + + def _get_one_rr_per_rrset(self, value): + # What the caller picked is fine. + return value + + # pylint: disable=unused-argument + + def _parse_rr_header(self, section, name, rdclass, rdtype): + return (rdclass, rdtype, None, False) + + # pylint: enable=unused-argument + + def _parse_special_rr_header(self, section, count, position, name, rdclass, rdtype): + if rdtype == dns.rdatatype.OPT: + if ( + section != MessageSection.ADDITIONAL + or self.opt + or name != dns.name.root + ): + raise BadEDNS + elif rdtype == dns.rdatatype.TSIG: + if ( + section != MessageSection.ADDITIONAL + or rdclass != dns.rdatatype.ANY + or position != count - 1 + ): + raise BadTSIG + return (rdclass, rdtype, None, False) + + +class ChainingResult: + """The result of a call to dns.message.QueryMessage.resolve_chaining(). + + The ``answer`` attribute is the answer RRSet, or ``None`` if it doesn't + exist. + + The ``canonical_name`` attribute is the canonical name after all + chaining has been applied (this is the same name as ``rrset.name`` in cases + where rrset is not ``None``). + + The ``minimum_ttl`` attribute is the minimum TTL, i.e. the TTL to + use if caching the data. It is the smallest of all the CNAME TTLs + and either the answer TTL if it exists or the SOA TTL and SOA + minimum values for negative answers. + + The ``cnames`` attribute is a list of all the CNAME RRSets followed to + get to the canonical name. + """ + + def __init__( + self, + canonical_name: dns.name.Name, + answer: dns.rrset.RRset | None, + minimum_ttl: int, + cnames: List[dns.rrset.RRset], + ): + self.canonical_name = canonical_name + self.answer = answer + self.minimum_ttl = minimum_ttl + self.cnames = cnames + + +class QueryMessage(Message): + def resolve_chaining(self) -> ChainingResult: + """Follow the CNAME chain in the response to determine the answer + RRset. + + Raises ``dns.message.NotQueryResponse`` if the message is not + a response. + + Raises ``dns.message.ChainTooLong`` if the CNAME chain is too long. + + Raises ``dns.message.AnswerForNXDOMAIN`` if the rcode is NXDOMAIN + but an answer was found. + + Raises ``dns.exception.FormError`` if the question count is not 1. + + Returns a ChainingResult object. + """ + if self.flags & dns.flags.QR == 0: + raise NotQueryResponse + if len(self.question) != 1: + raise dns.exception.FormError + question = self.question[0] + qname = question.name + min_ttl = dns.ttl.MAX_TTL + answer = None + count = 0 + cnames = [] + while count < MAX_CHAIN: + try: + answer = self.find_rrset( + self.answer, qname, question.rdclass, question.rdtype + ) + min_ttl = min(min_ttl, answer.ttl) + break + except KeyError: + if question.rdtype != dns.rdatatype.CNAME: + try: + crrset = self.find_rrset( + self.answer, qname, question.rdclass, dns.rdatatype.CNAME + ) + cnames.append(crrset) + min_ttl = min(min_ttl, crrset.ttl) + for rd in crrset: + qname = rd.target + break + count += 1 + continue + except KeyError: + # Exit the chaining loop + break + else: + # Exit the chaining loop + break + if count >= MAX_CHAIN: + raise ChainTooLong + if self.rcode() == dns.rcode.NXDOMAIN and answer is not None: + raise AnswerForNXDOMAIN + if answer is None: + # Further minimize the TTL with NCACHE. + auname = qname + while True: + # Look for an SOA RR whose owner name is a superdomain + # of qname. + try: + srrset = self.find_rrset( + self.authority, auname, question.rdclass, dns.rdatatype.SOA + ) + srdata = cast(dns.rdtypes.ANY.SOA.SOA, srrset[0]) + min_ttl = min(min_ttl, srrset.ttl, srdata.minimum) + break + except KeyError: + try: + auname = auname.parent() + except dns.name.NoParent: + break + return ChainingResult(qname, answer, min_ttl, cnames) + + def canonical_name(self) -> dns.name.Name: + """Return the canonical name of the first name in the question + section. + + Raises ``dns.message.NotQueryResponse`` if the message is not + a response. + + Raises ``dns.message.ChainTooLong`` if the CNAME chain is too long. + + Raises ``dns.message.AnswerForNXDOMAIN`` if the rcode is NXDOMAIN + but an answer was found. + + Raises ``dns.exception.FormError`` if the question count is not 1. + """ + return self.resolve_chaining().canonical_name + + +def _maybe_import_update(): + # We avoid circular imports by doing this here. We do it in another + # function as doing it in _message_factory_from_opcode() makes "dns" + # a local symbol, and the first line fails :) + + # pylint: disable=redefined-outer-name,import-outside-toplevel,unused-import + import dns.update # noqa: F401 + + +def _message_factory_from_opcode(opcode): + if opcode == dns.opcode.QUERY: + return QueryMessage + elif opcode == dns.opcode.UPDATE: + _maybe_import_update() + return dns.update.UpdateMessage # pyright: ignore + else: + return Message + + +class _WireReader: + """Wire format reader. + + parser: the binary parser + message: The message object being built + initialize_message: Callback to set message parsing options + question_only: Are we only reading the question? + one_rr_per_rrset: Put each RR into its own RRset? + keyring: TSIG keyring + ignore_trailing: Ignore trailing junk at end of request? + multi: Is this message part of a multi-message sequence? + DNS dynamic updates. + continue_on_error: try to extract as much information as possible from + the message, accumulating MessageErrors in the *errors* attribute instead of + raising them. + """ + + def __init__( + self, + wire, + initialize_message, + question_only=False, + one_rr_per_rrset=False, + ignore_trailing=False, + keyring=None, + multi=False, + continue_on_error=False, + ): + self.parser = dns.wire.Parser(wire) + self.message = None + self.initialize_message = initialize_message + self.question_only = question_only + self.one_rr_per_rrset = one_rr_per_rrset + self.ignore_trailing = ignore_trailing + self.keyring = keyring + self.multi = multi + self.continue_on_error = continue_on_error + self.errors = [] + + def _get_question(self, section_number, qcount): + """Read the next *qcount* records from the wire data and add them to + the question section. + """ + assert self.message is not None + section = self.message.sections[section_number] + for _ in range(qcount): + qname = self.parser.get_name(self.message.origin) + (rdtype, rdclass) = self.parser.get_struct("!HH") + (rdclass, rdtype, _, _) = self.message._parse_rr_header( + section_number, qname, rdclass, rdtype + ) + self.message.find_rrset( + section, qname, rdclass, rdtype, create=True, force_unique=True + ) + + def _add_error(self, e): + self.errors.append(MessageError(e, self.parser.current)) + + def _get_section(self, section_number, count): + """Read the next I{count} records from the wire data and add them to + the specified section. + + section_number: the section of the message to which to add records + count: the number of records to read + """ + assert self.message is not None + section = self.message.sections[section_number] + force_unique = self.one_rr_per_rrset + for i in range(count): + rr_start = self.parser.current + absolute_name = self.parser.get_name() + if self.message.origin is not None: + name = absolute_name.relativize(self.message.origin) + else: + name = absolute_name + (rdtype, rdclass, ttl, rdlen) = self.parser.get_struct("!HHIH") + if rdtype in (dns.rdatatype.OPT, dns.rdatatype.TSIG): + ( + rdclass, + rdtype, + deleting, + empty, + ) = self.message._parse_special_rr_header( + section_number, count, i, name, rdclass, rdtype + ) + else: + (rdclass, rdtype, deleting, empty) = self.message._parse_rr_header( + section_number, name, rdclass, rdtype + ) + rdata_start = self.parser.current + try: + if empty: + if rdlen > 0: + raise dns.exception.FormError + rd = None + covers = dns.rdatatype.NONE + else: + with self.parser.restrict_to(rdlen): + rd = dns.rdata.from_wire_parser( + rdclass, # pyright: ignore + rdtype, + self.parser, + self.message.origin, + ) + covers = rd.covers() + if self.message.xfr and rdtype == dns.rdatatype.SOA: + force_unique = True + if rdtype == dns.rdatatype.OPT: + self.message.opt = dns.rrset.from_rdata(name, ttl, rd) + elif rdtype == dns.rdatatype.TSIG: + trd = cast(dns.rdtypes.ANY.TSIG.TSIG, rd) + if self.keyring is None or self.keyring is True: + raise UnknownTSIGKey("got signed message without keyring") + elif isinstance(self.keyring, dict): + key = self.keyring.get(absolute_name) + if isinstance(key, bytes): + key = dns.tsig.Key(absolute_name, key, trd.algorithm) + elif callable(self.keyring): + key = self.keyring(self.message, absolute_name) + else: + key = self.keyring + if key is None: + raise UnknownTSIGKey(f"key '{name}' unknown") + if key: + self.message.keyring = key + self.message.tsig_ctx = dns.tsig.validate( + self.parser.wire, + key, + absolute_name, + rd, + int(time.time()), + self.message.request_mac, + rr_start, + self.message.tsig_ctx, + self.multi, + ) + self.message.tsig = dns.rrset.from_rdata(absolute_name, 0, rd) + else: + rrset = self.message.find_rrset( + section, + name, + rdclass, # pyright: ignore + rdtype, + covers, + deleting, + True, + force_unique, + ) + if rd is not None: + if ttl > 0x7FFFFFFF: + ttl = 0 + rrset.add(rd, ttl) + except Exception as e: + if self.continue_on_error: + self._add_error(e) + self.parser.seek(rdata_start + rdlen) + else: + raise + + def read(self): + """Read a wire format DNS message and build a dns.message.Message + object.""" + + if self.parser.remaining() < 12: + raise ShortHeader + (id, flags, qcount, ancount, aucount, adcount) = self.parser.get_struct( + "!HHHHHH" + ) + factory = _message_factory_from_opcode(dns.opcode.from_flags(flags)) + self.message = factory(id=id) + self.message.flags = dns.flags.Flag(flags) + self.message.wire = self.parser.wire + self.initialize_message(self.message) + self.one_rr_per_rrset = self.message._get_one_rr_per_rrset( + self.one_rr_per_rrset + ) + try: + self._get_question(MessageSection.QUESTION, qcount) + if self.question_only: + return self.message + self._get_section(MessageSection.ANSWER, ancount) + self._get_section(MessageSection.AUTHORITY, aucount) + self._get_section(MessageSection.ADDITIONAL, adcount) + if not self.ignore_trailing and self.parser.remaining() != 0: + raise TrailingJunk + if self.multi and self.message.tsig_ctx and not self.message.had_tsig: + self.message.tsig_ctx.update(self.parser.wire) + except Exception as e: + if self.continue_on_error: + self._add_error(e) + else: + raise + return self.message + + +def from_wire( + wire: bytes, + keyring: Any | None = None, + request_mac: bytes | None = b"", + xfr: bool = False, + origin: dns.name.Name | None = None, + tsig_ctx: dns.tsig.HMACTSig | dns.tsig.GSSTSig | None = None, + multi: bool = False, + question_only: bool = False, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + raise_on_truncation: bool = False, + continue_on_error: bool = False, +) -> Message: + """Convert a DNS wire format message into a message object. + + *keyring*, a ``dns.tsig.Key``, ``dict``, ``bool``, or ``None``, the key or keyring + to use if the message is signed. If ``None`` or ``True``, then trying to decode + a message with a TSIG will fail as it cannot be validated. If ``False``, then + TSIG validation is disabled. + + *request_mac*, a ``bytes`` or ``None``. If the message is a response to a + TSIG-signed request, *request_mac* should be set to the MAC of that request. + + *xfr*, a ``bool``, should be set to ``True`` if this message is part of a zone + transfer. + + *origin*, a ``dns.name.Name`` or ``None``. If the message is part of a zone + transfer, *origin* should be the origin name of the zone. If not ``None``, names + will be relativized to the origin. + + *tsig_ctx*, a ``dns.tsig.HMACTSig`` or ``dns.tsig.GSSTSig`` object, the ongoing TSIG + context, used when validating zone transfers. + + *multi*, a ``bool``, should be set to ``True`` if this message is part of a multiple + message sequence. + + *question_only*, a ``bool``. If ``True``, read only up to the end of the question + section. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own RRset. + + *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the + message. + + *raise_on_truncation*, a ``bool``. If ``True``, raise an exception if the TC bit is + set. + + *continue_on_error*, a ``bool``. If ``True``, try to continue parsing even if + errors occur. Erroneous rdata will be ignored. Errors will be accumulated as a + list of MessageError objects in the message's ``errors`` attribute. This option is + recommended only for DNS analysis tools, or for use in a server as part of an error + handling path. The default is ``False``. + + Raises ``dns.message.ShortHeader`` if the message is less than 12 octets long. + + Raises ``dns.message.TrailingJunk`` if there were octets in the message past the end + of the proper DNS message, and *ignore_trailing* is ``False``. + + Raises ``dns.message.BadEDNS`` if an OPT record was in the wrong section, or + occurred more than once. + + Raises ``dns.message.BadTSIG`` if a TSIG record was not the last record of the + additional data section. + + Raises ``dns.message.Truncated`` if the TC flag is set and *raise_on_truncation* is + ``True``. + + Returns a ``dns.message.Message``. + """ + + # We permit None for request_mac solely for backwards compatibility + if request_mac is None: + request_mac = b"" + + def initialize_message(message): + message.request_mac = request_mac + message.xfr = xfr + message.origin = origin + message.tsig_ctx = tsig_ctx + + reader = _WireReader( + wire, + initialize_message, + question_only, + one_rr_per_rrset, + ignore_trailing, + keyring, + multi, + continue_on_error, + ) + try: + m = reader.read() + except dns.exception.FormError: + if ( + reader.message + and (reader.message.flags & dns.flags.TC) + and raise_on_truncation + ): + raise Truncated(message=reader.message) + else: + raise + # Reading a truncated message might not have any errors, so we + # have to do this check here too. + if m.flags & dns.flags.TC and raise_on_truncation: + raise Truncated(message=m) + if continue_on_error: + m.errors = reader.errors + + return m + + +class _TextReader: + """Text format reader. + + tok: the tokenizer. + message: The message object being built. + DNS dynamic updates. + last_name: The most recently read name when building a message object. + one_rr_per_rrset: Put each RR into its own RRset? + origin: The origin for relative names + relativize: relativize names? + relativize_to: the origin to relativize to. + """ + + def __init__( + self, + text: str, + idna_codec: dns.name.IDNACodec | None, + one_rr_per_rrset: bool = False, + origin: dns.name.Name | None = None, + relativize: bool = True, + relativize_to: dns.name.Name | None = None, + ): + self.message: Message | None = None # mypy: ignore + self.tok = dns.tokenizer.Tokenizer(text, idna_codec=idna_codec) + self.last_name = None + self.one_rr_per_rrset = one_rr_per_rrset + self.origin = origin + self.relativize = relativize + self.relativize_to = relativize_to + self.id = None + self.edns = -1 + self.ednsflags = 0 + self.payload = DEFAULT_EDNS_PAYLOAD + self.rcode = None + self.opcode = dns.opcode.QUERY + self.flags = 0 + + def _header_line(self, _): + """Process one line from the text format header section.""" + + token = self.tok.get() + what = token.value + if what == "id": + self.id = self.tok.get_int() + elif what == "flags": + while True: + token = self.tok.get() + if not token.is_identifier(): + self.tok.unget(token) + break + self.flags = self.flags | dns.flags.from_text(token.value) + elif what == "edns": + self.edns = self.tok.get_int() + self.ednsflags = self.ednsflags | (self.edns << 16) + elif what == "eflags": + if self.edns < 0: + self.edns = 0 + while True: + token = self.tok.get() + if not token.is_identifier(): + self.tok.unget(token) + break + self.ednsflags = self.ednsflags | dns.flags.edns_from_text(token.value) + elif what == "payload": + self.payload = self.tok.get_int() + if self.edns < 0: + self.edns = 0 + elif what == "opcode": + text = self.tok.get_string() + self.opcode = dns.opcode.from_text(text) + self.flags = self.flags | dns.opcode.to_flags(self.opcode) + elif what == "rcode": + text = self.tok.get_string() + self.rcode = dns.rcode.from_text(text) + else: + raise UnknownHeaderField + self.tok.get_eol() + + def _question_line(self, section_number): + """Process one line from the text format question section.""" + + assert self.message is not None + section = self.message.sections[section_number] + token = self.tok.get(want_leading=True) + if not token.is_whitespace(): + self.last_name = self.tok.as_name( + token, self.message.origin, self.relativize, self.relativize_to + ) + name = self.last_name + if name is None: + raise NoPreviousName + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + # Class + try: + rdclass = dns.rdataclass.from_text(token.value) + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + except dns.exception.SyntaxError: + raise dns.exception.SyntaxError + except Exception: + rdclass = dns.rdataclass.IN + # Type + rdtype = dns.rdatatype.from_text(token.value) + (rdclass, rdtype, _, _) = self.message._parse_rr_header( + section_number, name, rdclass, rdtype + ) + self.message.find_rrset( + section, name, rdclass, rdtype, create=True, force_unique=True + ) + self.tok.get_eol() + + def _rr_line(self, section_number): + """Process one line from the text format answer, authority, or + additional data sections. + """ + + assert self.message is not None + section = self.message.sections[section_number] + # Name + token = self.tok.get(want_leading=True) + if not token.is_whitespace(): + self.last_name = self.tok.as_name( + token, self.message.origin, self.relativize, self.relativize_to + ) + name = self.last_name + if name is None: + raise NoPreviousName + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + # TTL + try: + ttl = int(token.value, 0) + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + except dns.exception.SyntaxError: + raise dns.exception.SyntaxError + except Exception: + ttl = 0 + # Class + try: + rdclass = dns.rdataclass.from_text(token.value) + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + except dns.exception.SyntaxError: + raise dns.exception.SyntaxError + except Exception: + rdclass = dns.rdataclass.IN + # Type + rdtype = dns.rdatatype.from_text(token.value) + (rdclass, rdtype, deleting, empty) = self.message._parse_rr_header( + section_number, name, rdclass, rdtype + ) + token = self.tok.get() + if empty and not token.is_eol_or_eof(): + raise dns.exception.SyntaxError + if not empty and token.is_eol_or_eof(): + raise dns.exception.UnexpectedEnd + if not token.is_eol_or_eof(): + self.tok.unget(token) + rd = dns.rdata.from_text( + rdclass, + rdtype, + self.tok, + self.message.origin, + self.relativize, + self.relativize_to, + ) + covers = rd.covers() + else: + rd = None + covers = dns.rdatatype.NONE + rrset = self.message.find_rrset( + section, + name, + rdclass, + rdtype, + covers, + deleting, + True, + self.one_rr_per_rrset, + ) + if rd is not None: + rrset.add(rd, ttl) + + def _make_message(self): + factory = _message_factory_from_opcode(self.opcode) + message = factory(id=self.id) + message.flags = self.flags + if self.edns >= 0: + message.use_edns(self.edns, self.ednsflags, self.payload) + if self.rcode: + message.set_rcode(self.rcode) + if self.origin: + message.origin = self.origin + return message + + def read(self): + """Read a text format DNS message and build a dns.message.Message + object.""" + + line_method = self._header_line + section_number = None + while 1: + token = self.tok.get(True, True) + if token.is_eol_or_eof(): + break + if token.is_comment(): + u = token.value.upper() + if u == "HEADER": + line_method = self._header_line + + if self.message: + message = self.message + else: + # If we don't have a message, create one with the current + # opcode, so that we know which section names to parse. + message = self._make_message() + try: + section_number = message._section_enum.from_text(u) + # We found a section name. If we don't have a message, + # use the one we just created. + if not self.message: + self.message = message + self.one_rr_per_rrset = message._get_one_rr_per_rrset( + self.one_rr_per_rrset + ) + if section_number == MessageSection.QUESTION: + line_method = self._question_line + else: + line_method = self._rr_line + except Exception: + # It's just a comment. + pass + self.tok.get_eol() + continue + self.tok.unget(token) + line_method(section_number) + if not self.message: + self.message = self._make_message() + return self.message + + +def from_text( + text: str, + idna_codec: dns.name.IDNACodec | None = None, + one_rr_per_rrset: bool = False, + origin: dns.name.Name | None = None, + relativize: bool = True, + relativize_to: dns.name.Name | None = None, +) -> Message: + """Convert the text format message into a message object. + + The reader stops after reading the first blank line in the input to + facilitate reading multiple messages from a single file with + ``dns.message.from_file()``. + + *text*, a ``str``, the text format message. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + *one_rr_per_rrset*, a ``bool``. If ``True``, then each RR is put + into its own rrset. The default is ``False``. + + *origin*, a ``dns.name.Name`` (or ``None``), the + origin to use for relative names. + + *relativize*, a ``bool``. If true, name will be relativized. + + *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use + when relativizing names. If not set, the *origin* value will be used. + + Raises ``dns.message.UnknownHeaderField`` if a header is unknown. + + Raises ``dns.exception.SyntaxError`` if the text is badly formed. + + Returns a ``dns.message.Message object`` + """ + + # 'text' can also be a file, but we don't publish that fact + # since it's an implementation detail. The official file + # interface is from_file(). + + reader = _TextReader( + text, idna_codec, one_rr_per_rrset, origin, relativize, relativize_to + ) + return reader.read() + + +def from_file( + f: Any, + idna_codec: dns.name.IDNACodec | None = None, + one_rr_per_rrset: bool = False, +) -> Message: + """Read the next text format message from the specified file. + + Message blocks are separated by a single blank line. + + *f*, a ``file`` or ``str``. If *f* is text, it is treated as the + pathname of a file to open. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + *one_rr_per_rrset*, a ``bool``. If ``True``, then each RR is put + into its own rrset. The default is ``False``. + + Raises ``dns.message.UnknownHeaderField`` if a header is unknown. + + Raises ``dns.exception.SyntaxError`` if the text is badly formed. + + Returns a ``dns.message.Message object`` + """ + + if isinstance(f, str): + cm: contextlib.AbstractContextManager = open(f, encoding="utf-8") + else: + cm = contextlib.nullcontext(f) + with cm as f: + return from_text(f, idna_codec, one_rr_per_rrset) + assert False # for mypy lgtm[py/unreachable-statement] + + +def make_query( + qname: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str, + rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + use_edns: int | bool | None = None, + want_dnssec: bool = False, + ednsflags: int | None = None, + payload: int | None = None, + request_payload: int | None = None, + options: List[dns.edns.Option] | None = None, + idna_codec: dns.name.IDNACodec | None = None, + id: int | None = None, + flags: int = dns.flags.RD, + pad: int = 0, +) -> QueryMessage: + """Make a query message. + + The query name, type, and class may all be specified either + as objects of the appropriate type, or as strings. + + The query will have a randomly chosen query id, and its DNS flags + will be set to dns.flags.RD. + + qname, a ``dns.name.Name`` or ``str``, the query name. + + *rdtype*, an ``int`` or ``str``, the desired rdata type. + + *rdclass*, an ``int`` or ``str``, the desired rdata class; the default + is class IN. + + *use_edns*, an ``int``, ``bool`` or ``None``. The EDNS level to use; the + default is ``None``. If ``None``, EDNS will be enabled only if other + parameters (*ednsflags*, *payload*, *request_payload*, or *options*) are + set. + See the description of dns.message.Message.use_edns() for the possible + values for use_edns and their meanings. + + *want_dnssec*, a ``bool``. If ``True``, DNSSEC data is desired. + + *ednsflags*, an ``int``, the EDNS flag values. + + *payload*, an ``int``, is the EDNS sender's payload field, which is the + maximum size of UDP datagram the sender can handle. I.e. how big + a response to this message can be. + + *request_payload*, an ``int``, is the EDNS payload size to use when + sending this message. If not specified, defaults to the value of + *payload*. + + *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS + options. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + *id*, an ``int`` or ``None``, the desired query id. The default is + ``None``, which generates a random query id. + + *flags*, an ``int``, the desired query flags. The default is + ``dns.flags.RD``. + + *pad*, a non-negative ``int``. If 0, the default, do not pad; otherwise add + padding bytes to make the message size a multiple of *pad*. Note that if + padding is non-zero, an EDNS PADDING option will always be added to the + message. + + Returns a ``dns.message.QueryMessage`` + """ + + if isinstance(qname, str): + qname = dns.name.from_text(qname, idna_codec=idna_codec) + rdtype = dns.rdatatype.RdataType.make(rdtype) + rdclass = dns.rdataclass.RdataClass.make(rdclass) + m = QueryMessage(id=id) + m.flags = dns.flags.Flag(flags) + m.find_rrset(m.question, qname, rdclass, rdtype, create=True, force_unique=True) + # only pass keywords on to use_edns if they have been set to a + # non-None value. Setting a field will turn EDNS on if it hasn't + # been configured. + kwargs: Dict[str, Any] = {} + if ednsflags is not None: + kwargs["ednsflags"] = ednsflags + if payload is not None: + kwargs["payload"] = payload + if request_payload is not None: + kwargs["request_payload"] = request_payload + if options is not None: + kwargs["options"] = options + if kwargs and use_edns is None: + use_edns = 0 + kwargs["edns"] = use_edns + kwargs["pad"] = pad + m.use_edns(**kwargs) + if want_dnssec: + m.want_dnssec(want_dnssec) + return m + + +class CopyMode(enum.Enum): + """ + How should sections be copied when making an update response? + """ + + NOTHING = 0 + QUESTION = 1 + EVERYTHING = 2 + + +def make_response( + query: Message, + recursion_available: bool = False, + our_payload: int = 8192, + fudge: int = 300, + tsig_error: int = 0, + pad: int | None = None, + copy_mode: CopyMode | None = None, +) -> Message: + """Make a message which is a response for the specified query. + The message returned is really a response skeleton; it has all of the infrastructure + required of a response, but none of the content. + + Response section(s) which are copied are shallow copies of the matching section(s) + in the query, so the query's RRsets should not be changed. + + *query*, a ``dns.message.Message``, the query to respond to. + + *recursion_available*, a ``bool``, should RA be set in the response? + + *our_payload*, an ``int``, the payload size to advertise in EDNS responses. + + *fudge*, an ``int``, the TSIG time fudge. + + *tsig_error*, an ``int``, the TSIG error. + + *pad*, a non-negative ``int`` or ``None``. If 0, the default, do not pad; otherwise + if not ``None`` add padding bytes to make the message size a multiple of *pad*. Note + that if padding is non-zero, an EDNS PADDING option will always be added to the + message. If ``None``, add padding following RFC 8467, namely if the request is + padded, pad the response to 468 otherwise do not pad. + + *copy_mode*, a ``dns.message.CopyMode`` or ``None``, determines how sections are + copied. The default, ``None`` copies sections according to the default for the + message's opcode, which is currently ``dns.message.CopyMode.QUESTION`` for all + opcodes. ``dns.message.CopyMode.QUESTION`` copies only the question section. + ``dns.message.CopyMode.EVERYTHING`` copies all sections other than OPT or TSIG + records, which are created appropriately if needed. ``dns.message.CopyMode.NOTHING`` + copies no sections; note that this mode is for server testing purposes and is + otherwise not recommended for use. In particular, ``dns.message.is_response()`` + will be ``False`` if you create a response this way and the rcode is not + ``FORMERR``, ``SERVFAIL``, ``NOTIMP``, or ``REFUSED``. + + Returns a ``dns.message.Message`` object whose specific class is appropriate for the + query. For example, if query is a ``dns.update.UpdateMessage``, the response will + be one too. + """ + + if query.flags & dns.flags.QR: + raise dns.exception.FormError("specified query message is not a query") + opcode = query.opcode() + factory = _message_factory_from_opcode(opcode) + response = factory(id=query.id) + response.flags = dns.flags.QR | (query.flags & dns.flags.RD) + if recursion_available: + response.flags |= dns.flags.RA + response.set_opcode(opcode) + if copy_mode is None: + copy_mode = CopyMode.QUESTION + if copy_mode != CopyMode.NOTHING: + response.question = list(query.question) + if copy_mode == CopyMode.EVERYTHING: + response.answer = list(query.answer) + response.authority = list(query.authority) + response.additional = list(query.additional) + if query.edns >= 0: + if pad is None: + # Set response padding per RFC 8467 + pad = 0 + for option in query.options: + if option.otype == dns.edns.OptionType.PADDING: + pad = 468 + response.use_edns(0, 0, our_payload, query.payload, pad=pad) + if query.had_tsig and query.keyring: + assert query.mac is not None + assert query.keyalgorithm is not None + response.use_tsig( + query.keyring, + query.keyname, + fudge, + None, + tsig_error, + b"", + query.keyalgorithm, + ) + response.request_mac = query.mac + return response + + +### BEGIN generated MessageSection constants + +QUESTION = MessageSection.QUESTION +ANSWER = MessageSection.ANSWER +AUTHORITY = MessageSection.AUTHORITY +ADDITIONAL = MessageSection.ADDITIONAL + +### END generated MessageSection constants diff --git a/netdeploy/lib/python3.11/site-packages/dns/name.py b/netdeploy/lib/python3.11/site-packages/dns/name.py new file mode 100644 index 0000000..45c8f45 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/name.py @@ -0,0 +1,1289 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Names.""" + +import copy +import encodings.idna # type: ignore +import functools +import struct +from typing import Any, Callable, Dict, Iterable, Optional, Tuple + +import dns._features +import dns.enum +import dns.exception +import dns.immutable +import dns.wire + +# Dnspython will never access idna if the import fails, but pyright can't figure +# that out, so... +# +# pyright: reportAttributeAccessIssue = false, reportPossiblyUnboundVariable = false + +if dns._features.have("idna"): + import idna # type: ignore + + have_idna_2008 = True +else: # pragma: no cover + have_idna_2008 = False + + +CompressType = Dict["Name", int] + + +class NameRelation(dns.enum.IntEnum): + """Name relation result from fullcompare().""" + + # This is an IntEnum for backwards compatibility in case anyone + # has hardwired the constants. + + #: The compared names have no relationship to each other. + NONE = 0 + #: the first name is a superdomain of the second. + SUPERDOMAIN = 1 + #: The first name is a subdomain of the second. + SUBDOMAIN = 2 + #: The compared names are equal. + EQUAL = 3 + #: The compared names have a common ancestor. + COMMONANCESTOR = 4 + + @classmethod + def _maximum(cls): + return cls.COMMONANCESTOR # pragma: no cover + + @classmethod + def _short_name(cls): + return cls.__name__ # pragma: no cover + + +# Backwards compatibility +NAMERELN_NONE = NameRelation.NONE +NAMERELN_SUPERDOMAIN = NameRelation.SUPERDOMAIN +NAMERELN_SUBDOMAIN = NameRelation.SUBDOMAIN +NAMERELN_EQUAL = NameRelation.EQUAL +NAMERELN_COMMONANCESTOR = NameRelation.COMMONANCESTOR + + +class EmptyLabel(dns.exception.SyntaxError): + """A DNS label is empty.""" + + +class BadEscape(dns.exception.SyntaxError): + """An escaped code in a text format of DNS name is invalid.""" + + +class BadPointer(dns.exception.FormError): + """A DNS compression pointer points forward instead of backward.""" + + +class BadLabelType(dns.exception.FormError): + """The label type in DNS name wire format is unknown.""" + + +class NeedAbsoluteNameOrOrigin(dns.exception.DNSException): + """An attempt was made to convert a non-absolute name to + wire when there was also a non-absolute (or missing) origin.""" + + +class NameTooLong(dns.exception.FormError): + """A DNS name is > 255 octets long.""" + + +class LabelTooLong(dns.exception.SyntaxError): + """A DNS label is > 63 octets long.""" + + +class AbsoluteConcatenation(dns.exception.DNSException): + """An attempt was made to append anything other than the + empty name to an absolute DNS name.""" + + +class NoParent(dns.exception.DNSException): + """An attempt was made to get the parent of the root name + or the empty name.""" + + +class NoIDNA2008(dns.exception.DNSException): + """IDNA 2008 processing was requested but the idna module is not + available.""" + + +class IDNAException(dns.exception.DNSException): + """IDNA processing raised an exception.""" + + supp_kwargs = {"idna_exception"} + fmt = "IDNA processing exception: {idna_exception}" + + # We do this as otherwise mypy complains about unexpected keyword argument + # idna_exception + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + +class NeedSubdomainOfOrigin(dns.exception.DNSException): + """An absolute name was provided that is not a subdomain of the specified origin.""" + + +_escaped = b'"().;\\@$' +_escaped_text = '"().;\\@$' + + +def _escapify(label: bytes | str) -> str: + """Escape the characters in label which need it. + @returns: the escaped string + @rtype: string""" + if isinstance(label, bytes): + # Ordinary DNS label mode. Escape special characters and values + # < 0x20 or > 0x7f. + text = "" + for c in label: + if c in _escaped: + text += "\\" + chr(c) + elif c > 0x20 and c < 0x7F: + text += chr(c) + else: + text += f"\\{c:03d}" + return text + + # Unicode label mode. Escape only special characters and values < 0x20 + text = "" + for uc in label: + if uc in _escaped_text: + text += "\\" + uc + elif uc <= "\x20": + text += f"\\{ord(uc):03d}" + else: + text += uc + return text + + +class IDNACodec: + """Abstract base class for IDNA encoder/decoders.""" + + def __init__(self): + pass + + def is_idna(self, label: bytes) -> bool: + return label.lower().startswith(b"xn--") + + def encode(self, label: str) -> bytes: + raise NotImplementedError # pragma: no cover + + def decode(self, label: bytes) -> str: + # We do not apply any IDNA policy on decode. + if self.is_idna(label): + try: + slabel = label[4:].decode("punycode") + return _escapify(slabel) + except Exception as e: + raise IDNAException(idna_exception=e) + else: + return _escapify(label) + + +class IDNA2003Codec(IDNACodec): + """IDNA 2003 encoder/decoder.""" + + def __init__(self, strict_decode: bool = False): + """Initialize the IDNA 2003 encoder/decoder. + + *strict_decode* is a ``bool``. If `True`, then IDNA2003 checking + is done when decoding. This can cause failures if the name + was encoded with IDNA2008. The default is `False`. + """ + + super().__init__() + self.strict_decode = strict_decode + + def encode(self, label: str) -> bytes: + """Encode *label*.""" + + if label == "": + return b"" + try: + return encodings.idna.ToASCII(label) + except UnicodeError: + raise LabelTooLong + + def decode(self, label: bytes) -> str: + """Decode *label*.""" + if not self.strict_decode: + return super().decode(label) + if label == b"": + return "" + try: + return _escapify(encodings.idna.ToUnicode(label)) + except Exception as e: + raise IDNAException(idna_exception=e) + + +class IDNA2008Codec(IDNACodec): + """IDNA 2008 encoder/decoder.""" + + def __init__( + self, + uts_46: bool = False, + transitional: bool = False, + allow_pure_ascii: bool = False, + strict_decode: bool = False, + ): + """Initialize the IDNA 2008 encoder/decoder. + + *uts_46* is a ``bool``. If True, apply Unicode IDNA + compatibility processing as described in Unicode Technical + Standard #46 (https://unicode.org/reports/tr46/). + If False, do not apply the mapping. The default is False. + + *transitional* is a ``bool``: If True, use the + "transitional" mode described in Unicode Technical Standard + #46. The default is False. + + *allow_pure_ascii* is a ``bool``. If True, then a label which + consists of only ASCII characters is allowed. This is less + strict than regular IDNA 2008, but is also necessary for mixed + names, e.g. a name with starting with "_sip._tcp." and ending + in an IDN suffix which would otherwise be disallowed. The + default is False. + + *strict_decode* is a ``bool``: If True, then IDNA2008 checking + is done when decoding. This can cause failures if the name + was encoded with IDNA2003. The default is False. + """ + super().__init__() + self.uts_46 = uts_46 + self.transitional = transitional + self.allow_pure_ascii = allow_pure_ascii + self.strict_decode = strict_decode + + def encode(self, label: str) -> bytes: + if label == "": + return b"" + if self.allow_pure_ascii and is_all_ascii(label): + encoded = label.encode("ascii") + if len(encoded) > 63: + raise LabelTooLong + return encoded + if not have_idna_2008: + raise NoIDNA2008 + try: + if self.uts_46: + # pylint: disable=possibly-used-before-assignment + label = idna.uts46_remap(label, False, self.transitional) + return idna.alabel(label) + except idna.IDNAError as e: + if e.args[0] == "Label too long": + raise LabelTooLong + else: + raise IDNAException(idna_exception=e) + + def decode(self, label: bytes) -> str: + if not self.strict_decode: + return super().decode(label) + if label == b"": + return "" + if not have_idna_2008: + raise NoIDNA2008 + try: + ulabel = idna.ulabel(label) + if self.uts_46: + ulabel = idna.uts46_remap(ulabel, False, self.transitional) + return _escapify(ulabel) + except (idna.IDNAError, UnicodeError) as e: + raise IDNAException(idna_exception=e) + + +IDNA_2003_Practical = IDNA2003Codec(False) +IDNA_2003_Strict = IDNA2003Codec(True) +IDNA_2003 = IDNA_2003_Practical +IDNA_2008_Practical = IDNA2008Codec(True, False, True, False) +IDNA_2008_UTS_46 = IDNA2008Codec(True, False, False, False) +IDNA_2008_Strict = IDNA2008Codec(False, False, False, True) +IDNA_2008_Transitional = IDNA2008Codec(True, True, False, False) +IDNA_2008 = IDNA_2008_Practical + + +def _validate_labels(labels: Tuple[bytes, ...]) -> None: + """Check for empty labels in the middle of a label sequence, + labels that are too long, and for too many labels. + + Raises ``dns.name.NameTooLong`` if the name as a whole is too long. + + Raises ``dns.name.EmptyLabel`` if a label is empty (i.e. the root + label) and appears in a position other than the end of the label + sequence + + """ + + l = len(labels) + total = 0 + i = -1 + j = 0 + for label in labels: + ll = len(label) + total += ll + 1 + if ll > 63: + raise LabelTooLong + if i < 0 and label == b"": + i = j + j += 1 + if total > 255: + raise NameTooLong + if i >= 0 and i != l - 1: + raise EmptyLabel + + +def _maybe_convert_to_binary(label: bytes | str) -> bytes: + """If label is ``str``, convert it to ``bytes``. If it is already + ``bytes`` just return it. + + """ + + if isinstance(label, bytes): + return label + if isinstance(label, str): + return label.encode() + raise ValueError # pragma: no cover + + +@dns.immutable.immutable +class Name: + """A DNS name. + + The dns.name.Name class represents a DNS name as a tuple of + labels. Each label is a ``bytes`` in DNS wire format. Instances + of the class are immutable. + """ + + __slots__ = ["labels"] + + def __init__(self, labels: Iterable[bytes | str]): + """*labels* is any iterable whose values are ``str`` or ``bytes``.""" + + blabels = [_maybe_convert_to_binary(x) for x in labels] + self.labels = tuple(blabels) + _validate_labels(self.labels) + + def __copy__(self): + return Name(self.labels) + + def __deepcopy__(self, memo): + return Name(copy.deepcopy(self.labels, memo)) + + def __getstate__(self): + # Names can be pickled + return {"labels": self.labels} + + def __setstate__(self, state): + super().__setattr__("labels", state["labels"]) + _validate_labels(self.labels) + + def is_absolute(self) -> bool: + """Is the most significant label of this name the root label? + + Returns a ``bool``. + """ + + return len(self.labels) > 0 and self.labels[-1] == b"" + + def is_wild(self) -> bool: + """Is this name wild? (I.e. Is the least significant label '*'?) + + Returns a ``bool``. + """ + + return len(self.labels) > 0 and self.labels[0] == b"*" + + def __hash__(self) -> int: + """Return a case-insensitive hash of the name. + + Returns an ``int``. + """ + + h = 0 + for label in self.labels: + for c in label.lower(): + h += (h << 3) + c + return h + + def fullcompare(self, other: "Name") -> Tuple[NameRelation, int, int]: + """Compare two names, returning a 3-tuple + ``(relation, order, nlabels)``. + + *relation* describes the relation ship between the names, + and is one of: ``dns.name.NameRelation.NONE``, + ``dns.name.NameRelation.SUPERDOMAIN``, ``dns.name.NameRelation.SUBDOMAIN``, + ``dns.name.NameRelation.EQUAL``, or ``dns.name.NameRelation.COMMONANCESTOR``. + + *order* is < 0 if *self* < *other*, > 0 if *self* > *other*, and == + 0 if *self* == *other*. A relative name is always less than an + absolute name. If both names have the same relativity, then + the DNSSEC order relation is used to order them. + + *nlabels* is the number of significant labels that the two names + have in common. + + Here are some examples. Names ending in "." are absolute names, + those not ending in "." are relative names. + + ============= ============= =========== ===== ======= + self other relation order nlabels + ============= ============= =========== ===== ======= + www.example. www.example. equal 0 3 + www.example. example. subdomain > 0 2 + example. www.example. superdomain < 0 2 + example1.com. example2.com. common anc. < 0 2 + example1 example2. none < 0 0 + example1. example2 none > 0 0 + ============= ============= =========== ===== ======= + """ + + sabs = self.is_absolute() + oabs = other.is_absolute() + if sabs != oabs: + if sabs: + return (NameRelation.NONE, 1, 0) + else: + return (NameRelation.NONE, -1, 0) + l1 = len(self.labels) + l2 = len(other.labels) + ldiff = l1 - l2 + if ldiff < 0: + l = l1 + else: + l = l2 + + order = 0 + nlabels = 0 + namereln = NameRelation.NONE + while l > 0: + l -= 1 + l1 -= 1 + l2 -= 1 + label1 = self.labels[l1].lower() + label2 = other.labels[l2].lower() + if label1 < label2: + order = -1 + if nlabels > 0: + namereln = NameRelation.COMMONANCESTOR + return (namereln, order, nlabels) + elif label1 > label2: + order = 1 + if nlabels > 0: + namereln = NameRelation.COMMONANCESTOR + return (namereln, order, nlabels) + nlabels += 1 + order = ldiff + if ldiff < 0: + namereln = NameRelation.SUPERDOMAIN + elif ldiff > 0: + namereln = NameRelation.SUBDOMAIN + else: + namereln = NameRelation.EQUAL + return (namereln, order, nlabels) + + def is_subdomain(self, other: "Name") -> bool: + """Is self a subdomain of other? + + Note that the notion of subdomain includes equality, e.g. + "dnspython.org" is a subdomain of itself. + + Returns a ``bool``. + """ + + (nr, _, _) = self.fullcompare(other) + if nr == NameRelation.SUBDOMAIN or nr == NameRelation.EQUAL: + return True + return False + + def is_superdomain(self, other: "Name") -> bool: + """Is self a superdomain of other? + + Note that the notion of superdomain includes equality, e.g. + "dnspython.org" is a superdomain of itself. + + Returns a ``bool``. + """ + + (nr, _, _) = self.fullcompare(other) + if nr == NameRelation.SUPERDOMAIN or nr == NameRelation.EQUAL: + return True + return False + + def canonicalize(self) -> "Name": + """Return a name which is equal to the current name, but is in + DNSSEC canonical form. + """ + + return Name([x.lower() for x in self.labels]) + + def __eq__(self, other): + if isinstance(other, Name): + return self.fullcompare(other)[1] == 0 + else: + return False + + def __ne__(self, other): + if isinstance(other, Name): + return self.fullcompare(other)[1] != 0 + else: + return True + + def __lt__(self, other): + if isinstance(other, Name): + return self.fullcompare(other)[1] < 0 + else: + return NotImplemented + + def __le__(self, other): + if isinstance(other, Name): + return self.fullcompare(other)[1] <= 0 + else: + return NotImplemented + + def __ge__(self, other): + if isinstance(other, Name): + return self.fullcompare(other)[1] >= 0 + else: + return NotImplemented + + def __gt__(self, other): + if isinstance(other, Name): + return self.fullcompare(other)[1] > 0 + else: + return NotImplemented + + def __repr__(self): + return "" + + def __str__(self): + return self.to_text(False) + + def to_text(self, omit_final_dot: bool = False) -> str: + """Convert name to DNS text format. + + *omit_final_dot* is a ``bool``. If True, don't emit the final + dot (denoting the root label) for absolute names. The default + is False. + + Returns a ``str``. + """ + + if len(self.labels) == 0: + return "@" + if len(self.labels) == 1 and self.labels[0] == b"": + return "." + if omit_final_dot and self.is_absolute(): + l = self.labels[:-1] + else: + l = self.labels + s = ".".join(map(_escapify, l)) + return s + + def to_unicode( + self, omit_final_dot: bool = False, idna_codec: IDNACodec | None = None + ) -> str: + """Convert name to Unicode text format. + + IDN ACE labels are converted to Unicode. + + *omit_final_dot* is a ``bool``. If True, don't emit the final + dot (denoting the root label) for absolute names. The default + is False. + *idna_codec* specifies the IDNA encoder/decoder. If None, the + dns.name.IDNA_2003_Practical encoder/decoder is used. + The IDNA_2003_Practical decoder does + not impose any policy, it just decodes punycode, so if you + don't want checking for compliance, you can use this decoder + for IDNA2008 as well. + + Returns a ``str``. + """ + + if len(self.labels) == 0: + return "@" + if len(self.labels) == 1 and self.labels[0] == b"": + return "." + if omit_final_dot and self.is_absolute(): + l = self.labels[:-1] + else: + l = self.labels + if idna_codec is None: + idna_codec = IDNA_2003_Practical + return ".".join([idna_codec.decode(x) for x in l]) + + def to_digestable(self, origin: Optional["Name"] = None) -> bytes: + """Convert name to a format suitable for digesting in hashes. + + The name is canonicalized and converted to uncompressed wire + format. All names in wire format are absolute. If the name + is a relative name, then an origin must be supplied. + + *origin* is a ``dns.name.Name`` or ``None``. If the name is + relative and origin is not ``None``, then origin will be appended + to the name. + + Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is + relative and no origin was provided. + + Returns a ``bytes``. + """ + + digest = self.to_wire(origin=origin, canonicalize=True) + assert digest is not None + return digest + + def to_wire( + self, + file: Any | None = None, + compress: CompressType | None = None, + origin: Optional["Name"] = None, + canonicalize: bool = False, + ) -> bytes | None: + """Convert name to wire format, possibly compressing it. + + *file* is the file where the name is emitted (typically an + io.BytesIO file). If ``None`` (the default), a ``bytes`` + containing the wire name will be returned. + + *compress*, a ``dict``, is the compression table to use. If + ``None`` (the default), names will not be compressed. Note that + the compression code assumes that compression offset 0 is the + start of *file*, and thus compression will not be correct + if this is not the case. + + *origin* is a ``dns.name.Name`` or ``None``. If the name is + relative and origin is not ``None``, then *origin* will be appended + to it. + + *canonicalize*, a ``bool``, indicates whether the name should + be canonicalized; that is, converted to a format suitable for + digesting in hashes. + + Raises ``dns.name.NeedAbsoluteNameOrOrigin`` if the name is + relative and no origin was provided. + + Returns a ``bytes`` or ``None``. + """ + + if file is None: + out = bytearray() + for label in self.labels: + out.append(len(label)) + if canonicalize: + out += label.lower() + else: + out += label + if not self.is_absolute(): + if origin is None or not origin.is_absolute(): + raise NeedAbsoluteNameOrOrigin + for label in origin.labels: + out.append(len(label)) + if canonicalize: + out += label.lower() + else: + out += label + return bytes(out) + + labels: Iterable[bytes] + if not self.is_absolute(): + if origin is None or not origin.is_absolute(): + raise NeedAbsoluteNameOrOrigin + labels = list(self.labels) + labels.extend(list(origin.labels)) + else: + labels = self.labels + i = 0 + for label in labels: + n = Name(labels[i:]) + i += 1 + if compress is not None: + pos = compress.get(n) + else: + pos = None + if pos is not None: + value = 0xC000 + pos + s = struct.pack("!H", value) + file.write(s) + break + else: + if compress is not None and len(n) > 1: + pos = file.tell() + if pos <= 0x3FFF: + compress[n] = pos + l = len(label) + file.write(struct.pack("!B", l)) + if l > 0: + if canonicalize: + file.write(label.lower()) + else: + file.write(label) + return None + + def __len__(self) -> int: + """The length of the name (in labels). + + Returns an ``int``. + """ + + return len(self.labels) + + def __getitem__(self, index): + return self.labels[index] + + def __add__(self, other): + return self.concatenate(other) + + def __sub__(self, other): + return self.relativize(other) + + def split(self, depth: int) -> Tuple["Name", "Name"]: + """Split a name into a prefix and suffix names at the specified depth. + + *depth* is an ``int`` specifying the number of labels in the suffix + + Raises ``ValueError`` if *depth* was not >= 0 and <= the length of the + name. + + Returns the tuple ``(prefix, suffix)``. + """ + + l = len(self.labels) + if depth == 0: + return (self, dns.name.empty) + elif depth == l: + return (dns.name.empty, self) + elif depth < 0 or depth > l: + raise ValueError("depth must be >= 0 and <= the length of the name") + return (Name(self[:-depth]), Name(self[-depth:])) + + def concatenate(self, other: "Name") -> "Name": + """Return a new name which is the concatenation of self and other. + + Raises ``dns.name.AbsoluteConcatenation`` if the name is + absolute and *other* is not the empty name. + + Returns a ``dns.name.Name``. + """ + + if self.is_absolute() and len(other) > 0: + raise AbsoluteConcatenation + labels = list(self.labels) + labels.extend(list(other.labels)) + return Name(labels) + + def relativize(self, origin: "Name") -> "Name": + """If the name is a subdomain of *origin*, return a new name which is + the name relative to origin. Otherwise return the name. + + For example, relativizing ``www.dnspython.org.`` to origin + ``dnspython.org.`` returns the name ``www``. Relativizing ``example.`` + to origin ``dnspython.org.`` returns ``example.``. + + Returns a ``dns.name.Name``. + """ + + if origin is not None and self.is_subdomain(origin): + return Name(self[: -len(origin)]) + else: + return self + + def derelativize(self, origin: "Name") -> "Name": + """If the name is a relative name, return a new name which is the + concatenation of the name and origin. Otherwise return the name. + + For example, derelativizing ``www`` to origin ``dnspython.org.`` + returns the name ``www.dnspython.org.``. Derelativizing ``example.`` + to origin ``dnspython.org.`` returns ``example.``. + + Returns a ``dns.name.Name``. + """ + + if not self.is_absolute(): + return self.concatenate(origin) + else: + return self + + def choose_relativity( + self, origin: Optional["Name"] = None, relativize: bool = True + ) -> "Name": + """Return a name with the relativity desired by the caller. + + If *origin* is ``None``, then the name is returned. + Otherwise, if *relativize* is ``True`` the name is + relativized, and if *relativize* is ``False`` the name is + derelativized. + + Returns a ``dns.name.Name``. + """ + + if origin: + if relativize: + return self.relativize(origin) + else: + return self.derelativize(origin) + else: + return self + + def parent(self) -> "Name": + """Return the parent of the name. + + For example, the parent of ``www.dnspython.org.`` is ``dnspython.org``. + + Raises ``dns.name.NoParent`` if the name is either the root name or the + empty name, and thus has no parent. + + Returns a ``dns.name.Name``. + """ + + if self == root or self == empty: + raise NoParent + return Name(self.labels[1:]) + + def predecessor(self, origin: "Name", prefix_ok: bool = True) -> "Name": + """Return the maximal predecessor of *name* in the DNSSEC ordering in the zone + whose origin is *origin*, or return the longest name under *origin* if the + name is origin (i.e. wrap around to the longest name, which may still be + *origin* due to length considerations. + + The relativity of the name is preserved, so if this name is relative + then the method will return a relative name, and likewise if this name + is absolute then the predecessor will be absolute. + + *prefix_ok* indicates if prefixing labels is allowed, and + defaults to ``True``. Normally it is good to allow this, but if computing + a maximal predecessor at a zone cut point then ``False`` must be specified. + """ + return _handle_relativity_and_call( + _absolute_predecessor, self, origin, prefix_ok + ) + + def successor(self, origin: "Name", prefix_ok: bool = True) -> "Name": + """Return the minimal successor of *name* in the DNSSEC ordering in the zone + whose origin is *origin*, or return *origin* if the successor cannot be + computed due to name length limitations. + + Note that *origin* is returned in the "too long" cases because wrapping + around to the origin is how NSEC records express "end of the zone". + + The relativity of the name is preserved, so if this name is relative + then the method will return a relative name, and likewise if this name + is absolute then the successor will be absolute. + + *prefix_ok* indicates if prefixing a new minimal label is allowed, and + defaults to ``True``. Normally it is good to allow this, but if computing + a minimal successor at a zone cut point then ``False`` must be specified. + """ + return _handle_relativity_and_call(_absolute_successor, self, origin, prefix_ok) + + +#: The root name, '.' +root = Name([b""]) + +#: The empty name. +empty = Name([]) + + +def from_unicode( + text: str, origin: Name | None = root, idna_codec: IDNACodec | None = None +) -> Name: + """Convert unicode text into a Name object. + + Labels are encoded in IDN ACE form according to rules specified by + the IDNA codec. + + *text*, a ``str``, is the text to convert into a name. + + *origin*, a ``dns.name.Name``, specifies the origin to + append to non-absolute names. The default is the root name. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + Returns a ``dns.name.Name``. + """ + + if not isinstance(text, str): + raise ValueError("input to from_unicode() must be a unicode string") + if not (origin is None or isinstance(origin, Name)): + raise ValueError("origin must be a Name or None") + labels = [] + label = "" + escaping = False + edigits = 0 + total = 0 + if idna_codec is None: + idna_codec = IDNA_2003 + if text == "@": + text = "" + if text: + if text in [".", "\u3002", "\uff0e", "\uff61"]: + return Name([b""]) # no Unicode "u" on this constant! + for c in text: + if escaping: + if edigits == 0: + if c.isdigit(): + total = int(c) + edigits += 1 + else: + label += c + escaping = False + else: + if not c.isdigit(): + raise BadEscape + total *= 10 + total += int(c) + edigits += 1 + if edigits == 3: + escaping = False + label += chr(total) + elif c in [".", "\u3002", "\uff0e", "\uff61"]: + if len(label) == 0: + raise EmptyLabel + labels.append(idna_codec.encode(label)) + label = "" + elif c == "\\": + escaping = True + edigits = 0 + total = 0 + else: + label += c + if escaping: + raise BadEscape + if len(label) > 0: + labels.append(idna_codec.encode(label)) + else: + labels.append(b"") + + if (len(labels) == 0 or labels[-1] != b"") and origin is not None: + labels.extend(list(origin.labels)) + return Name(labels) + + +def is_all_ascii(text: str) -> bool: + for c in text: + if ord(c) > 0x7F: + return False + return True + + +def from_text( + text: bytes | str, + origin: Name | None = root, + idna_codec: IDNACodec | None = None, +) -> Name: + """Convert text into a Name object. + + *text*, a ``bytes`` or ``str``, is the text to convert into a name. + + *origin*, a ``dns.name.Name``, specifies the origin to + append to non-absolute names. The default is the root name. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + Returns a ``dns.name.Name``. + """ + + if isinstance(text, str): + if not is_all_ascii(text): + # Some codepoint in the input text is > 127, so IDNA applies. + return from_unicode(text, origin, idna_codec) + # The input is all ASCII, so treat this like an ordinary non-IDNA + # domain name. Note that "all ASCII" is about the input text, + # not the codepoints in the domain name. E.g. if text has value + # + # r'\150\151\152\153\154\155\156\157\158\159' + # + # then it's still "all ASCII" even though the domain name has + # codepoints > 127. + text = text.encode("ascii") + if not isinstance(text, bytes): + raise ValueError("input to from_text() must be a string") + if not (origin is None or isinstance(origin, Name)): + raise ValueError("origin must be a Name or None") + labels = [] + label = b"" + escaping = False + edigits = 0 + total = 0 + if text == b"@": + text = b"" + if text: + if text == b".": + return Name([b""]) + for c in text: + byte_ = struct.pack("!B", c) + if escaping: + if edigits == 0: + if byte_.isdigit(): + total = int(byte_) + edigits += 1 + else: + label += byte_ + escaping = False + else: + if not byte_.isdigit(): + raise BadEscape + total *= 10 + total += int(byte_) + edigits += 1 + if edigits == 3: + escaping = False + label += struct.pack("!B", total) + elif byte_ == b".": + if len(label) == 0: + raise EmptyLabel + labels.append(label) + label = b"" + elif byte_ == b"\\": + escaping = True + edigits = 0 + total = 0 + else: + label += byte_ + if escaping: + raise BadEscape + if len(label) > 0: + labels.append(label) + else: + labels.append(b"") + if (len(labels) == 0 or labels[-1] != b"") and origin is not None: + labels.extend(list(origin.labels)) + return Name(labels) + + +# we need 'dns.wire.Parser' quoted as dns.name and dns.wire depend on each other. + + +def from_wire_parser(parser: "dns.wire.Parser") -> Name: + """Convert possibly compressed wire format into a Name. + + *parser* is a dns.wire.Parser. + + Raises ``dns.name.BadPointer`` if a compression pointer did not + point backwards in the message. + + Raises ``dns.name.BadLabelType`` if an invalid label type was encountered. + + Returns a ``dns.name.Name`` + """ + + labels = [] + biggest_pointer = parser.current + with parser.restore_furthest(): + count = parser.get_uint8() + while count != 0: + if count < 64: + labels.append(parser.get_bytes(count)) + elif count >= 192: + current = (count & 0x3F) * 256 + parser.get_uint8() + if current >= biggest_pointer: + raise BadPointer + biggest_pointer = current + parser.seek(current) + else: + raise BadLabelType + count = parser.get_uint8() + labels.append(b"") + return Name(labels) + + +def from_wire(message: bytes, current: int) -> Tuple[Name, int]: + """Convert possibly compressed wire format into a Name. + + *message* is a ``bytes`` containing an entire DNS message in DNS + wire form. + + *current*, an ``int``, is the offset of the beginning of the name + from the start of the message + + Raises ``dns.name.BadPointer`` if a compression pointer did not + point backwards in the message. + + Raises ``dns.name.BadLabelType`` if an invalid label type was encountered. + + Returns a ``(dns.name.Name, int)`` tuple consisting of the name + that was read and the number of bytes of the wire format message + which were consumed reading it. + """ + + if not isinstance(message, bytes): + raise ValueError("input to from_wire() must be a byte string") + parser = dns.wire.Parser(message, current) + name = from_wire_parser(parser) + return (name, parser.current - current) + + +# RFC 4471 Support + +_MINIMAL_OCTET = b"\x00" +_MINIMAL_OCTET_VALUE = ord(_MINIMAL_OCTET) +_SUCCESSOR_PREFIX = Name([_MINIMAL_OCTET]) +_MAXIMAL_OCTET = b"\xff" +_MAXIMAL_OCTET_VALUE = ord(_MAXIMAL_OCTET) +_AT_SIGN_VALUE = ord("@") +_LEFT_SQUARE_BRACKET_VALUE = ord("[") + + +def _wire_length(labels): + return functools.reduce(lambda v, x: v + len(x) + 1, labels, 0) + + +def _pad_to_max_name(name): + needed = 255 - _wire_length(name.labels) + new_labels = [] + while needed > 64: + new_labels.append(_MAXIMAL_OCTET * 63) + needed -= 64 + if needed >= 2: + new_labels.append(_MAXIMAL_OCTET * (needed - 1)) + # Note we're already maximal in the needed == 1 case as while we'd like + # to add one more byte as a new label, we can't, as adding a new non-empty + # label requires at least 2 bytes. + new_labels = list(reversed(new_labels)) + new_labels.extend(name.labels) + return Name(new_labels) + + +def _pad_to_max_label(label, suffix_labels): + length = len(label) + # We have to subtract one here to account for the length byte of label. + remaining = 255 - _wire_length(suffix_labels) - length - 1 + if remaining <= 0: + # Shouldn't happen! + return label + needed = min(63 - length, remaining) + return label + _MAXIMAL_OCTET * needed + + +def _absolute_predecessor(name: Name, origin: Name, prefix_ok: bool) -> Name: + # This is the RFC 4471 predecessor algorithm using the "absolute method" of section + # 3.1.1. + # + # Our caller must ensure that the name and origin are absolute, and that name is a + # subdomain of origin. + if name == origin: + return _pad_to_max_name(name) + least_significant_label = name[0] + if least_significant_label == _MINIMAL_OCTET: + return name.parent() + least_octet = least_significant_label[-1] + suffix_labels = name.labels[1:] + if least_octet == _MINIMAL_OCTET_VALUE: + new_labels = [least_significant_label[:-1]] + else: + octets = bytearray(least_significant_label) + octet = octets[-1] + if octet == _LEFT_SQUARE_BRACKET_VALUE: + octet = _AT_SIGN_VALUE + else: + octet -= 1 + octets[-1] = octet + least_significant_label = bytes(octets) + new_labels = [_pad_to_max_label(least_significant_label, suffix_labels)] + new_labels.extend(suffix_labels) + name = Name(new_labels) + if prefix_ok: + return _pad_to_max_name(name) + else: + return name + + +def _absolute_successor(name: Name, origin: Name, prefix_ok: bool) -> Name: + # This is the RFC 4471 successor algorithm using the "absolute method" of section + # 3.1.2. + # + # Our caller must ensure that the name and origin are absolute, and that name is a + # subdomain of origin. + if prefix_ok: + # Try prefixing \000 as new label + try: + return _SUCCESSOR_PREFIX.concatenate(name) + except NameTooLong: + pass + while name != origin: + # Try extending the least significant label. + least_significant_label = name[0] + if len(least_significant_label) < 63: + # We may be able to extend the least label with a minimal additional byte. + # This is only "may" because we could have a maximal length name even though + # the least significant label isn't maximally long. + new_labels = [least_significant_label + _MINIMAL_OCTET] + new_labels.extend(name.labels[1:]) + try: + return dns.name.Name(new_labels) + except dns.name.NameTooLong: + pass + # We can't extend the label either, so we'll try to increment the least + # signficant non-maximal byte in it. + octets = bytearray(least_significant_label) + # We do this reversed iteration with an explicit indexing variable because + # if we find something to increment, we're going to want to truncate everything + # to the right of it. + for i in range(len(octets) - 1, -1, -1): + octet = octets[i] + if octet == _MAXIMAL_OCTET_VALUE: + # We can't increment this, so keep looking. + continue + # Finally, something we can increment. We have to apply a special rule for + # incrementing "@", sending it to "[", because RFC 4034 6.1 says that when + # comparing names, uppercase letters compare as if they were their + # lower-case equivalents. If we increment "@" to "A", then it would compare + # as "a", which is after "[", "\", "]", "^", "_", and "`", so we would have + # skipped the most minimal successor, namely "[". + if octet == _AT_SIGN_VALUE: + octet = _LEFT_SQUARE_BRACKET_VALUE + else: + octet += 1 + octets[i] = octet + # We can now truncate all of the maximal values we skipped (if any) + new_labels = [bytes(octets[: i + 1])] + new_labels.extend(name.labels[1:]) + # We haven't changed the length of the name, so the Name constructor will + # always work. + return Name(new_labels) + # We couldn't increment, so chop off the least significant label and try + # again. + name = name.parent() + + # We couldn't increment at all, so return the origin, as wrapping around is the + # DNSSEC way. + return origin + + +def _handle_relativity_and_call( + function: Callable[[Name, Name, bool], Name], + name: Name, + origin: Name, + prefix_ok: bool, +) -> Name: + # Make "name" absolute if needed, ensure that the origin is absolute, + # call function(), and then relativize the result if needed. + if not origin.is_absolute(): + raise NeedAbsoluteNameOrOrigin + relative = not name.is_absolute() + if relative: + name = name.derelativize(origin) + elif not name.is_subdomain(origin): + raise NeedSubdomainOfOrigin + result_name = function(name, origin, prefix_ok) + if relative: + result_name = result_name.relativize(origin) + return result_name diff --git a/netdeploy/lib/python3.11/site-packages/dns/namedict.py b/netdeploy/lib/python3.11/site-packages/dns/namedict.py new file mode 100644 index 0000000..ca8b197 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/namedict.py @@ -0,0 +1,109 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# Copyright (C) 2016 Coresec Systems AB +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND CORESEC SYSTEMS AB DISCLAIMS ALL +# WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +# WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL CORESEC +# SYSTEMS AB BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR +# CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS +# OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, +# NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION +# WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS name dictionary""" + +# pylint seems to be confused about this one! +from collections.abc import MutableMapping # pylint: disable=no-name-in-module + +import dns.name + + +class NameDict(MutableMapping): + """A dictionary whose keys are dns.name.Name objects. + + In addition to being like a regular Python dictionary, this + dictionary can also get the deepest match for a given key. + """ + + __slots__ = ["max_depth", "max_depth_items", "__store"] + + def __init__(self, *args, **kwargs): + super().__init__() + self.__store = dict() + #: the maximum depth of the keys that have ever been added + self.max_depth = 0 + #: the number of items of maximum depth + self.max_depth_items = 0 + self.update(dict(*args, **kwargs)) + + def __update_max_depth(self, key): + if len(key) == self.max_depth: + self.max_depth_items = self.max_depth_items + 1 + elif len(key) > self.max_depth: + self.max_depth = len(key) + self.max_depth_items = 1 + + def __getitem__(self, key): + return self.__store[key] + + def __setitem__(self, key, value): + if not isinstance(key, dns.name.Name): + raise ValueError("NameDict key must be a name") + self.__store[key] = value + self.__update_max_depth(key) + + def __delitem__(self, key): + self.__store.pop(key) + if len(key) == self.max_depth: + self.max_depth_items = self.max_depth_items - 1 + if self.max_depth_items == 0: + self.max_depth = 0 + for k in self.__store: + self.__update_max_depth(k) + + def __iter__(self): + return iter(self.__store) + + def __len__(self): + return len(self.__store) + + def has_key(self, key): + return key in self.__store + + def get_deepest_match(self, name): + """Find the deepest match to *name* in the dictionary. + + The deepest match is the longest name in the dictionary which is + a superdomain of *name*. Note that *superdomain* includes matching + *name* itself. + + *name*, a ``dns.name.Name``, the name to find. + + Returns a ``(key, value)`` where *key* is the deepest + ``dns.name.Name``, and *value* is the value associated with *key*. + """ + + depth = len(name) + if depth > self.max_depth: + depth = self.max_depth + for i in range(-depth, 0): + n = dns.name.Name(name[i:]) + if n in self: + return (n, self[n]) + v = self[dns.name.empty] + return (dns.name.empty, v) diff --git a/netdeploy/lib/python3.11/site-packages/dns/nameserver.py b/netdeploy/lib/python3.11/site-packages/dns/nameserver.py new file mode 100644 index 0000000..c9307d3 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/nameserver.py @@ -0,0 +1,361 @@ +from urllib.parse import urlparse + +import dns.asyncbackend +import dns.asyncquery +import dns.message +import dns.query + + +class Nameserver: + def __init__(self): + pass + + def __str__(self): + raise NotImplementedError + + def kind(self) -> str: + raise NotImplementedError + + def is_always_max_size(self) -> bool: + raise NotImplementedError + + def answer_nameserver(self) -> str: + raise NotImplementedError + + def answer_port(self) -> int: + raise NotImplementedError + + def query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: str | None, + source_port: int, + max_size: bool, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + ) -> dns.message.Message: + raise NotImplementedError + + async def async_query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: str | None, + source_port: int, + max_size: bool, + backend: dns.asyncbackend.Backend, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + ) -> dns.message.Message: + raise NotImplementedError + + +class AddressAndPortNameserver(Nameserver): + def __init__(self, address: str, port: int): + super().__init__() + self.address = address + self.port = port + + def kind(self) -> str: + raise NotImplementedError + + def is_always_max_size(self) -> bool: + return False + + def __str__(self): + ns_kind = self.kind() + return f"{ns_kind}:{self.address}@{self.port}" + + def answer_nameserver(self) -> str: + return self.address + + def answer_port(self) -> int: + return self.port + + +class Do53Nameserver(AddressAndPortNameserver): + def __init__(self, address: str, port: int = 53): + super().__init__(address, port) + + def kind(self): + return "Do53" + + def query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: str | None, + source_port: int, + max_size: bool, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + ) -> dns.message.Message: + if max_size: + response = dns.query.tcp( + request, + self.address, + timeout=timeout, + port=self.port, + source=source, + source_port=source_port, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ) + else: + response = dns.query.udp( + request, + self.address, + timeout=timeout, + port=self.port, + source=source, + source_port=source_port, + raise_on_truncation=True, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ignore_errors=True, + ignore_unexpected=True, + ) + return response + + async def async_query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: str | None, + source_port: int, + max_size: bool, + backend: dns.asyncbackend.Backend, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + ) -> dns.message.Message: + if max_size: + response = await dns.asyncquery.tcp( + request, + self.address, + timeout=timeout, + port=self.port, + source=source, + source_port=source_port, + backend=backend, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ) + else: + response = await dns.asyncquery.udp( + request, + self.address, + timeout=timeout, + port=self.port, + source=source, + source_port=source_port, + raise_on_truncation=True, + backend=backend, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ignore_errors=True, + ignore_unexpected=True, + ) + return response + + +class DoHNameserver(Nameserver): + def __init__( + self, + url: str, + bootstrap_address: str | None = None, + verify: bool | str = True, + want_get: bool = False, + http_version: dns.query.HTTPVersion = dns.query.HTTPVersion.DEFAULT, + ): + super().__init__() + self.url = url + self.bootstrap_address = bootstrap_address + self.verify = verify + self.want_get = want_get + self.http_version = http_version + + def kind(self): + return "DoH" + + def is_always_max_size(self) -> bool: + return True + + def __str__(self): + return self.url + + def answer_nameserver(self) -> str: + return self.url + + def answer_port(self) -> int: + port = urlparse(self.url).port + if port is None: + port = 443 + return port + + def query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: str | None, + source_port: int, + max_size: bool = False, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + ) -> dns.message.Message: + return dns.query.https( + request, + self.url, + timeout=timeout, + source=source, + source_port=source_port, + bootstrap_address=self.bootstrap_address, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + verify=self.verify, + post=(not self.want_get), + http_version=self.http_version, + ) + + async def async_query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: str | None, + source_port: int, + max_size: bool, + backend: dns.asyncbackend.Backend, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + ) -> dns.message.Message: + return await dns.asyncquery.https( + request, + self.url, + timeout=timeout, + source=source, + source_port=source_port, + bootstrap_address=self.bootstrap_address, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + verify=self.verify, + post=(not self.want_get), + http_version=self.http_version, + ) + + +class DoTNameserver(AddressAndPortNameserver): + def __init__( + self, + address: str, + port: int = 853, + hostname: str | None = None, + verify: bool | str = True, + ): + super().__init__(address, port) + self.hostname = hostname + self.verify = verify + + def kind(self): + return "DoT" + + def query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: str | None, + source_port: int, + max_size: bool = False, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + ) -> dns.message.Message: + return dns.query.tls( + request, + self.address, + port=self.port, + timeout=timeout, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + server_hostname=self.hostname, + verify=self.verify, + ) + + async def async_query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: str | None, + source_port: int, + max_size: bool, + backend: dns.asyncbackend.Backend, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + ) -> dns.message.Message: + return await dns.asyncquery.tls( + request, + self.address, + port=self.port, + timeout=timeout, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + server_hostname=self.hostname, + verify=self.verify, + ) + + +class DoQNameserver(AddressAndPortNameserver): + def __init__( + self, + address: str, + port: int = 853, + verify: bool | str = True, + server_hostname: str | None = None, + ): + super().__init__(address, port) + self.verify = verify + self.server_hostname = server_hostname + + def kind(self): + return "DoQ" + + def query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: str | None, + source_port: int, + max_size: bool = False, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + ) -> dns.message.Message: + return dns.query.quic( + request, + self.address, + port=self.port, + timeout=timeout, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + verify=self.verify, + server_hostname=self.server_hostname, + ) + + async def async_query( + self, + request: dns.message.QueryMessage, + timeout: float, + source: str | None, + source_port: int, + max_size: bool, + backend: dns.asyncbackend.Backend, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + ) -> dns.message.Message: + return await dns.asyncquery.quic( + request, + self.address, + port=self.port, + timeout=timeout, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + verify=self.verify, + server_hostname=self.server_hostname, + ) diff --git a/netdeploy/lib/python3.11/site-packages/dns/node.py b/netdeploy/lib/python3.11/site-packages/dns/node.py new file mode 100644 index 0000000..b2cbf1b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/node.py @@ -0,0 +1,358 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS nodes. A node is a set of rdatasets.""" + +import enum +import io +from typing import Any, Dict + +import dns.immutable +import dns.name +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.rrset + +_cname_types = { + dns.rdatatype.CNAME, +} + +# "neutral" types can coexist with a CNAME and thus are not "other data" +_neutral_types = { + dns.rdatatype.NSEC, # RFC 4035 section 2.5 + dns.rdatatype.NSEC3, # This is not likely to happen, but not impossible! + dns.rdatatype.KEY, # RFC 4035 section 2.5, RFC 3007 +} + + +def _matches_type_or_its_signature(rdtypes, rdtype, covers): + return rdtype in rdtypes or (rdtype == dns.rdatatype.RRSIG and covers in rdtypes) + + +@enum.unique +class NodeKind(enum.Enum): + """Rdatasets in nodes""" + + REGULAR = 0 # a.k.a "other data" + NEUTRAL = 1 + CNAME = 2 + + @classmethod + def classify( + cls, rdtype: dns.rdatatype.RdataType, covers: dns.rdatatype.RdataType + ) -> "NodeKind": + if _matches_type_or_its_signature(_cname_types, rdtype, covers): + return NodeKind.CNAME + elif _matches_type_or_its_signature(_neutral_types, rdtype, covers): + return NodeKind.NEUTRAL + else: + return NodeKind.REGULAR + + @classmethod + def classify_rdataset(cls, rdataset: dns.rdataset.Rdataset) -> "NodeKind": + return cls.classify(rdataset.rdtype, rdataset.covers) + + +class Node: + """A Node is a set of rdatasets. + + A node is either a CNAME node or an "other data" node. A CNAME + node contains only CNAME, KEY, NSEC, and NSEC3 rdatasets along with their + covering RRSIG rdatasets. An "other data" node contains any + rdataset other than a CNAME or RRSIG(CNAME) rdataset. When + changes are made to a node, the CNAME or "other data" state is + always consistent with the update, i.e. the most recent change + wins. For example, if you have a node which contains a CNAME + rdataset, and then add an MX rdataset to it, then the CNAME + rdataset will be deleted. Likewise if you have a node containing + an MX rdataset and add a CNAME rdataset, the MX rdataset will be + deleted. + """ + + __slots__ = ["rdatasets"] + + def __init__(self): + # the set of rdatasets, represented as a list. + self.rdatasets = [] + + def to_text(self, name: dns.name.Name, **kw: Dict[str, Any]) -> str: + """Convert a node to text format. + + Each rdataset at the node is printed. Any keyword arguments + to this method are passed on to the rdataset's to_text() method. + + *name*, a ``dns.name.Name``, the owner name of the + rdatasets. + + Returns a ``str``. + + """ + + s = io.StringIO() + for rds in self.rdatasets: + if len(rds) > 0: + s.write(rds.to_text(name, **kw)) # type: ignore[arg-type] + s.write("\n") + return s.getvalue()[:-1] + + def __repr__(self): + return "" + + def __eq__(self, other): + # + # This is inefficient. Good thing we don't need to do it much. + # + for rd in self.rdatasets: + if rd not in other.rdatasets: + return False + for rd in other.rdatasets: + if rd not in self.rdatasets: + return False + return True + + def __ne__(self, other): + return not self.__eq__(other) + + def __len__(self): + return len(self.rdatasets) + + def __iter__(self): + return iter(self.rdatasets) + + def _append_rdataset(self, rdataset): + """Append rdataset to the node with special handling for CNAME and + other data conditions. + + Specifically, if the rdataset being appended has ``NodeKind.CNAME``, + then all rdatasets other than KEY, NSEC, NSEC3, and their covering + RRSIGs are deleted. If the rdataset being appended has + ``NodeKind.REGULAR`` then CNAME and RRSIG(CNAME) are deleted. + """ + # Make having just one rdataset at the node fast. + if len(self.rdatasets) > 0: + kind = NodeKind.classify_rdataset(rdataset) + if kind == NodeKind.CNAME: + self.rdatasets = [ + rds + for rds in self.rdatasets + if NodeKind.classify_rdataset(rds) != NodeKind.REGULAR + ] + elif kind == NodeKind.REGULAR: + self.rdatasets = [ + rds + for rds in self.rdatasets + if NodeKind.classify_rdataset(rds) != NodeKind.CNAME + ] + # Otherwise the rdataset is NodeKind.NEUTRAL and we do not need to + # edit self.rdatasets. + self.rdatasets.append(rdataset) + + def find_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset: + """Find an rdataset matching the specified properties in the + current node. + + *rdclass*, a ``dns.rdataclass.RdataClass``, the class of the rdataset. + + *rdtype*, a ``dns.rdatatype.RdataType``, the type of the rdataset. + + *covers*, a ``dns.rdatatype.RdataType``, the covered type. + Usually this value is ``dns.rdatatype.NONE``, but if the + rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, + then the covers value will be the rdata type the SIG/RRSIG + covers. The library treats the SIG and RRSIG types as if they + were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). + This makes RRSIGs much easier to work with than if RRSIGs + covering different rdata types were aggregated into a single + RRSIG rdataset. + + *create*, a ``bool``. If True, create the rdataset if it is not found. + + Raises ``KeyError`` if an rdataset of the desired type and class does + not exist and *create* is not ``True``. + + Returns a ``dns.rdataset.Rdataset``. + """ + + for rds in self.rdatasets: + if rds.match(rdclass, rdtype, covers): + return rds + if not create: + raise KeyError + rds = dns.rdataset.Rdataset(rdclass, rdtype, covers) + self._append_rdataset(rds) + return rds + + def get_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset | None: + """Get an rdataset matching the specified properties in the + current node. + + None is returned if an rdataset of the specified type and + class does not exist and *create* is not ``True``. + + *rdclass*, an ``int``, the class of the rdataset. + + *rdtype*, an ``int``, the type of the rdataset. + + *covers*, an ``int``, the covered type. Usually this value is + dns.rdatatype.NONE, but if the rdtype is dns.rdatatype.SIG or + dns.rdatatype.RRSIG, then the covers value will be the rdata + type the SIG/RRSIG covers. The library treats the SIG and RRSIG + types as if they were a family of + types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). This makes RRSIGs much + easier to work with than if RRSIGs covering different rdata + types were aggregated into a single RRSIG rdataset. + + *create*, a ``bool``. If True, create the rdataset if it is not found. + + Returns a ``dns.rdataset.Rdataset`` or ``None``. + """ + + try: + rds = self.find_rdataset(rdclass, rdtype, covers, create) + except KeyError: + rds = None + return rds + + def delete_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + ) -> None: + """Delete the rdataset matching the specified properties in the + current node. + + If a matching rdataset does not exist, it is not an error. + + *rdclass*, an ``int``, the class of the rdataset. + + *rdtype*, an ``int``, the type of the rdataset. + + *covers*, an ``int``, the covered type. + """ + + rds = self.get_rdataset(rdclass, rdtype, covers) + if rds is not None: + self.rdatasets.remove(rds) + + def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None: + """Replace an rdataset. + + It is not an error if there is no rdataset matching *replacement*. + + Ownership of the *replacement* object is transferred to the node; + in other words, this method does not store a copy of *replacement* + at the node, it stores *replacement* itself. + + *replacement*, a ``dns.rdataset.Rdataset``. + + Raises ``ValueError`` if *replacement* is not a + ``dns.rdataset.Rdataset``. + """ + + if not isinstance(replacement, dns.rdataset.Rdataset): + raise ValueError("replacement is not an rdataset") + if isinstance(replacement, dns.rrset.RRset): + # RRsets are not good replacements as the match() method + # is not compatible. + replacement = replacement.to_rdataset() + self.delete_rdataset( + replacement.rdclass, replacement.rdtype, replacement.covers + ) + self._append_rdataset(replacement) + + def classify(self) -> NodeKind: + """Classify a node. + + A node which contains a CNAME or RRSIG(CNAME) is a + ``NodeKind.CNAME`` node. + + A node which contains only "neutral" types, i.e. types allowed to + co-exist with a CNAME, is a ``NodeKind.NEUTRAL`` node. The neutral + types are NSEC, NSEC3, KEY, and their associated RRSIGS. An empty node + is also considered neutral. + + A node which contains some rdataset which is not a CNAME, RRSIG(CNAME), + or a neutral type is a a ``NodeKind.REGULAR`` node. Regular nodes are + also commonly referred to as "other data". + """ + for rdataset in self.rdatasets: + kind = NodeKind.classify(rdataset.rdtype, rdataset.covers) + if kind != NodeKind.NEUTRAL: + return kind + return NodeKind.NEUTRAL + + def is_immutable(self) -> bool: + return False + + +@dns.immutable.immutable +class ImmutableNode(Node): + def __init__(self, node): + super().__init__() + self.rdatasets = tuple( + [dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets] + ) + + def find_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset: + if create: + raise TypeError("immutable") + return super().find_rdataset(rdclass, rdtype, covers, False) + + def get_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset | None: + if create: + raise TypeError("immutable") + return super().get_rdataset(rdclass, rdtype, covers, False) + + def delete_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + ) -> None: + raise TypeError("immutable") + + def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None: + raise TypeError("immutable") + + def is_immutable(self) -> bool: + return True diff --git a/netdeploy/lib/python3.11/site-packages/dns/opcode.py b/netdeploy/lib/python3.11/site-packages/dns/opcode.py new file mode 100644 index 0000000..3fa610d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/opcode.py @@ -0,0 +1,119 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Opcodes.""" + +from typing import Type + +import dns.enum +import dns.exception + + +class Opcode(dns.enum.IntEnum): + #: Query + QUERY = 0 + #: Inverse Query (historical) + IQUERY = 1 + #: Server Status (unspecified and unimplemented anywhere) + STATUS = 2 + #: Notify + NOTIFY = 4 + #: Dynamic Update + UPDATE = 5 + + @classmethod + def _maximum(cls): + return 15 + + @classmethod + def _unknown_exception_class(cls) -> Type[Exception]: + return UnknownOpcode + + +class UnknownOpcode(dns.exception.DNSException): + """An DNS opcode is unknown.""" + + +def from_text(text: str) -> Opcode: + """Convert text into an opcode. + + *text*, a ``str``, the textual opcode + + Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown. + + Returns an ``int``. + """ + + return Opcode.from_text(text) + + +def from_flags(flags: int) -> Opcode: + """Extract an opcode from DNS message flags. + + *flags*, an ``int``, the DNS flags. + + Returns an ``int``. + """ + + return Opcode((flags & 0x7800) >> 11) + + +def to_flags(value: Opcode) -> int: + """Convert an opcode to a value suitable for ORing into DNS message + flags. + + *value*, an ``int``, the DNS opcode value. + + Returns an ``int``. + """ + + return (value << 11) & 0x7800 + + +def to_text(value: Opcode) -> str: + """Convert an opcode to text. + + *value*, an ``int`` the opcode value, + + Raises ``dns.opcode.UnknownOpcode`` if the opcode is unknown. + + Returns a ``str``. + """ + + return Opcode.to_text(value) + + +def is_update(flags: int) -> bool: + """Is the opcode in flags UPDATE? + + *flags*, an ``int``, the DNS message flags. + + Returns a ``bool``. + """ + + return from_flags(flags) == Opcode.UPDATE + + +### BEGIN generated Opcode constants + +QUERY = Opcode.QUERY +IQUERY = Opcode.IQUERY +STATUS = Opcode.STATUS +NOTIFY = Opcode.NOTIFY +UPDATE = Opcode.UPDATE + +### END generated Opcode constants diff --git a/netdeploy/lib/python3.11/site-packages/dns/py.typed b/netdeploy/lib/python3.11/site-packages/dns/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/dns/query.py b/netdeploy/lib/python3.11/site-packages/dns/query.py new file mode 100644 index 0000000..17b1862 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/query.py @@ -0,0 +1,1786 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Talk to a DNS server.""" + +import base64 +import contextlib +import enum +import errno +import os +import random +import selectors +import socket +import struct +import time +import urllib.parse +from typing import Any, Callable, Dict, Optional, Tuple, cast + +import dns._features +import dns._tls_util +import dns.exception +import dns.inet +import dns.message +import dns.name +import dns.quic +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.transaction +import dns.tsig +import dns.xfr + +try: + import ssl +except ImportError: + import dns._no_ssl as ssl # type: ignore + + +def _remaining(expiration): + if expiration is None: + return None + timeout = expiration - time.time() + if timeout <= 0.0: + raise dns.exception.Timeout + return timeout + + +def _expiration_for_this_attempt(timeout, expiration): + if expiration is None: + return None + return min(time.time() + timeout, expiration) + + +_have_httpx = dns._features.have("doh") +if _have_httpx: + import httpcore._backends.sync + import httpx + + _CoreNetworkBackend = httpcore.NetworkBackend + _CoreSyncStream = httpcore._backends.sync.SyncStream + + class _NetworkBackend(_CoreNetworkBackend): + def __init__(self, resolver, local_port, bootstrap_address, family): + super().__init__() + self._local_port = local_port + self._resolver = resolver + self._bootstrap_address = bootstrap_address + self._family = family + + def connect_tcp( + self, host, port, timeout=None, local_address=None, socket_options=None + ): # pylint: disable=signature-differs + addresses = [] + _, expiration = _compute_times(timeout) + if dns.inet.is_address(host): + addresses.append(host) + elif self._bootstrap_address is not None: + addresses.append(self._bootstrap_address) + else: + timeout = _remaining(expiration) + family = self._family + if local_address: + family = dns.inet.af_for_address(local_address) + answers = self._resolver.resolve_name( + host, family=family, lifetime=timeout + ) + addresses = answers.addresses() + for address in addresses: + af = dns.inet.af_for_address(address) + if local_address is not None or self._local_port != 0: + if local_address is None: + local_address = "0.0.0.0" + source = dns.inet.low_level_address_tuple( + (local_address, self._local_port), af + ) + else: + source = None + try: + sock = make_socket(af, socket.SOCK_STREAM, source) + attempt_expiration = _expiration_for_this_attempt(2.0, expiration) + _connect( + sock, + dns.inet.low_level_address_tuple((address, port), af), + attempt_expiration, + ) + return _CoreSyncStream(sock) + except Exception: + pass + raise httpcore.ConnectError + + def connect_unix_socket( + self, path, timeout=None, socket_options=None + ): # pylint: disable=signature-differs + raise NotImplementedError + + class _HTTPTransport(httpx.HTTPTransport): # pyright: ignore + def __init__( + self, + *args, + local_port=0, + bootstrap_address=None, + resolver=None, + family=socket.AF_UNSPEC, + **kwargs, + ): + if resolver is None and bootstrap_address is None: + # pylint: disable=import-outside-toplevel,redefined-outer-name + import dns.resolver + + resolver = dns.resolver.Resolver() + super().__init__(*args, **kwargs) + self._pool._network_backend = _NetworkBackend( + resolver, local_port, bootstrap_address, family + ) + +else: + + class _HTTPTransport: # type: ignore + def __init__( + self, + *args, + local_port=0, + bootstrap_address=None, + resolver=None, + family=socket.AF_UNSPEC, + **kwargs, + ): + pass + + def connect_tcp(self, host, port, timeout, local_address): + raise NotImplementedError + + +have_doh = _have_httpx + + +def default_socket_factory( + af: socket.AddressFamily | int, + kind: socket.SocketKind, + proto: int, +) -> socket.socket: + return socket.socket(af, kind, proto) + + +# Function used to create a socket. Can be overridden if needed in special +# situations. +socket_factory: Callable[ + [socket.AddressFamily | int, socket.SocketKind, int], socket.socket +] = default_socket_factory + + +class UnexpectedSource(dns.exception.DNSException): + """A DNS query response came from an unexpected address or port.""" + + +class BadResponse(dns.exception.FormError): + """A DNS query response does not respond to the question asked.""" + + +class NoDOH(dns.exception.DNSException): + """DNS over HTTPS (DOH) was requested but the httpx module is not + available.""" + + +class NoDOQ(dns.exception.DNSException): + """DNS over QUIC (DOQ) was requested but the aioquic module is not + available.""" + + +# for backwards compatibility +TransferError = dns.xfr.TransferError + + +def _compute_times(timeout): + now = time.time() + if timeout is None: + return (now, None) + else: + return (now, now + timeout) + + +def _wait_for(fd, readable, writable, _, expiration): + # Use the selected selector class to wait for any of the specified + # events. An "expiration" absolute time is converted into a relative + # timeout. + # + # The unused parameter is 'error', which is always set when + # selecting for read or write, and we have no error-only selects. + + if readable and isinstance(fd, ssl.SSLSocket) and fd.pending() > 0: + return True + with selectors.DefaultSelector() as sel: + events = 0 + if readable: + events |= selectors.EVENT_READ + if writable: + events |= selectors.EVENT_WRITE + if events: + sel.register(fd, events) # pyright: ignore + if expiration is None: + timeout = None + else: + timeout = expiration - time.time() + if timeout <= 0.0: + raise dns.exception.Timeout + if not sel.select(timeout): + raise dns.exception.Timeout + + +def _wait_for_readable(s, expiration): + _wait_for(s, True, False, True, expiration) + + +def _wait_for_writable(s, expiration): + _wait_for(s, False, True, True, expiration) + + +def _addresses_equal(af, a1, a2): + # Convert the first value of the tuple, which is a textual format + # address into binary form, so that we are not confused by different + # textual representations of the same address + try: + n1 = dns.inet.inet_pton(af, a1[0]) + n2 = dns.inet.inet_pton(af, a2[0]) + except dns.exception.SyntaxError: + return False + return n1 == n2 and a1[1:] == a2[1:] + + +def _matches_destination(af, from_address, destination, ignore_unexpected): + # Check that from_address is appropriate for a response to a query + # sent to destination. + if not destination: + return True + if _addresses_equal(af, from_address, destination) or ( + dns.inet.is_multicast(destination[0]) and from_address[1:] == destination[1:] + ): + return True + elif ignore_unexpected: + return False + raise UnexpectedSource( + f"got a response from {from_address} instead of " f"{destination}" + ) + + +def _destination_and_source( + where, port, source, source_port, where_must_be_address=True +): + # Apply defaults and compute destination and source tuples + # suitable for use in connect(), sendto(), or bind(). + af = None + destination = None + try: + af = dns.inet.af_for_address(where) + destination = where + except Exception: + if where_must_be_address: + raise + # URLs are ok so eat the exception + if source: + saf = dns.inet.af_for_address(source) + if af: + # We know the destination af, so source had better agree! + if saf != af: + raise ValueError( + "different address families for source and destination" + ) + else: + # We didn't know the destination af, but we know the source, + # so that's our af. + af = saf + if source_port and not source: + # Caller has specified a source_port but not an address, so we + # need to return a source, and we need to use the appropriate + # wildcard address as the address. + try: + source = dns.inet.any_for_af(af) + except Exception: + # we catch this and raise ValueError for backwards compatibility + raise ValueError("source_port specified but address family is unknown") + # Convert high-level (address, port) tuples into low-level address + # tuples. + if destination: + destination = dns.inet.low_level_address_tuple((destination, port), af) + if source: + source = dns.inet.low_level_address_tuple((source, source_port), af) + return (af, destination, source) + + +def make_socket( + af: socket.AddressFamily | int, + type: socket.SocketKind, + source: Any | None = None, +) -> socket.socket: + """Make a socket. + + This function uses the module's ``socket_factory`` to make a socket of the + specified address family and type. + + *af*, a ``socket.AddressFamily`` or ``int`` is the address family, either + ``socket.AF_INET`` or ``socket.AF_INET6``. + + *type*, a ``socket.SocketKind`` is the type of socket, e.g. ``socket.SOCK_DGRAM``, + a datagram socket, or ``socket.SOCK_STREAM``, a stream socket. Note that the + ``proto`` attribute of a socket is always zero with this API, so a datagram socket + will always be a UDP socket, and a stream socket will always be a TCP socket. + + *source* is the source address and port to bind to, if any. The default is + ``None`` which will bind to the wildcard address and a randomly chosen port. + If not ``None``, it should be a (low-level) address tuple appropriate for *af*. + """ + s = socket_factory(af, type, 0) + try: + s.setblocking(False) + if source is not None: + s.bind(source) + return s + except Exception: + s.close() + raise + + +def make_ssl_socket( + af: socket.AddressFamily | int, + type: socket.SocketKind, + ssl_context: ssl.SSLContext, + server_hostname: dns.name.Name | str | None = None, + source: Any | None = None, +) -> ssl.SSLSocket: + """Make a socket. + + This function uses the module's ``socket_factory`` to make a socket of the + specified address family and type. + + *af*, a ``socket.AddressFamily`` or ``int`` is the address family, either + ``socket.AF_INET`` or ``socket.AF_INET6``. + + *type*, a ``socket.SocketKind`` is the type of socket, e.g. ``socket.SOCK_DGRAM``, + a datagram socket, or ``socket.SOCK_STREAM``, a stream socket. Note that the + ``proto`` attribute of a socket is always zero with this API, so a datagram socket + will always be a UDP socket, and a stream socket will always be a TCP socket. + + If *ssl_context* is not ``None``, then it specifies the SSL context to use, + typically created with ``make_ssl_context()``. + + If *server_hostname* is not ``None``, then it is the hostname to use for server + certificate validation. A valid hostname must be supplied if *ssl_context* + requires hostname checking. + + *source* is the source address and port to bind to, if any. The default is + ``None`` which will bind to the wildcard address and a randomly chosen port. + If not ``None``, it should be a (low-level) address tuple appropriate for *af*. + """ + sock = make_socket(af, type, source) + if isinstance(server_hostname, dns.name.Name): + server_hostname = server_hostname.to_text() + # LGTM gets a false positive here, as our default context is OK + return ssl_context.wrap_socket( + sock, + do_handshake_on_connect=False, # lgtm[py/insecure-protocol] + server_hostname=server_hostname, + ) + + +# for backwards compatibility +def _make_socket( + af, + type, + source, + ssl_context, + server_hostname, +): + if ssl_context is not None: + return make_ssl_socket(af, type, ssl_context, server_hostname, source) + else: + return make_socket(af, type, source) + + +def _maybe_get_resolver( + resolver: Optional["dns.resolver.Resolver"], # pyright: ignore +) -> "dns.resolver.Resolver": # pyright: ignore + # We need a separate method for this to avoid overriding the global + # variable "dns" with the as-yet undefined local variable "dns" + # in https(). + if resolver is None: + # pylint: disable=import-outside-toplevel,redefined-outer-name + import dns.resolver + + resolver = dns.resolver.Resolver() + return resolver + + +class HTTPVersion(enum.IntEnum): + """Which version of HTTP should be used? + + DEFAULT will select the first version from the list [2, 1.1, 3] that + is available. + """ + + DEFAULT = 0 + HTTP_1 = 1 + H1 = 1 + HTTP_2 = 2 + H2 = 2 + HTTP_3 = 3 + H3 = 3 + + +def https( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 443, + source: str | None = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + session: Any | None = None, + path: str = "/dns-query", + post: bool = True, + bootstrap_address: str | None = None, + verify: bool | str | ssl.SSLContext = True, + resolver: Optional["dns.resolver.Resolver"] = None, # pyright: ignore + family: int = socket.AF_UNSPEC, + http_version: HTTPVersion = HTTPVersion.DEFAULT, +) -> dns.message.Message: + """Return the response obtained after sending a query via DNS-over-HTTPS. + + *q*, a ``dns.message.Message``, the query to send. + + *where*, a ``str``, the nameserver IP address or the full URL. If an IP address is + given, the URL will be constructed using the following schema: + https://:/. + + *timeout*, a ``float`` or ``None``, the number of seconds to wait before the query + times out. If ``None``, the default, wait forever. + + *port*, a ``int``, the port to send the query to. The default is 443. + + *source*, a ``str`` containing an IPv4 or IPv6 address, specifying the source + address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. The default is + 0. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own RRset. + + *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the + received message. + + *session*, an ``httpx.Client``. If provided, the client session to use to send the + queries. + + *path*, a ``str``. If *where* is an IP address, then *path* will be used to + construct the URL to send the DNS query to. + + *post*, a ``bool``. If ``True``, the default, POST method will be used. + + *bootstrap_address*, a ``str``, the IP address to use to bypass resolution. + + *verify*, a ``bool`` or ``str``. If a ``True``, then TLS certificate verification + of the server is done using the default CA bundle; if ``False``, then no + verification is done; if a `str` then it specifies the path to a certificate file or + directory which will be used for verification. + + *resolver*, a ``dns.resolver.Resolver`` or ``None``, the resolver to use for + resolution of hostnames in URLs. If not specified, a new resolver with a default + configuration will be used; note this is *not* the default resolver as that resolver + might have been configured to use DoH causing a chicken-and-egg problem. This + parameter only has an effect if the HTTP library is httpx. + + *family*, an ``int``, the address family. If socket.AF_UNSPEC (the default), both A + and AAAA records will be retrieved. + + *http_version*, a ``dns.query.HTTPVersion``, indicating which HTTP version to use. + + Returns a ``dns.message.Message``. + """ + + (af, _, the_source) = _destination_and_source( + where, port, source, source_port, False + ) + # we bind url and then override as pyright can't figure out all paths bind. + url = where + if af is not None and dns.inet.is_address(where): + if af == socket.AF_INET: + url = f"https://{where}:{port}{path}" + elif af == socket.AF_INET6: + url = f"https://[{where}]:{port}{path}" + + extensions = {} + if bootstrap_address is None: + # pylint: disable=possibly-used-before-assignment + parsed = urllib.parse.urlparse(url) + if parsed.hostname is None: + raise ValueError("no hostname in URL") + if dns.inet.is_address(parsed.hostname): + bootstrap_address = parsed.hostname + extensions["sni_hostname"] = parsed.hostname + if parsed.port is not None: + port = parsed.port + + if http_version == HTTPVersion.H3 or ( + http_version == HTTPVersion.DEFAULT and not have_doh + ): + if bootstrap_address is None: + resolver = _maybe_get_resolver(resolver) + assert parsed.hostname is not None # pyright: ignore + answers = resolver.resolve_name(parsed.hostname, family) # pyright: ignore + bootstrap_address = random.choice(list(answers.addresses())) + if session and not isinstance( + session, dns.quic.SyncQuicConnection + ): # pyright: ignore + raise ValueError("session parameter must be a dns.quic.SyncQuicConnection.") + return _http3( + q, + bootstrap_address, + url, # pyright: ignore + timeout, + port, + source, + source_port, + one_rr_per_rrset, + ignore_trailing, + verify=verify, + post=post, + connection=session, + ) + + if not have_doh: + raise NoDOH # pragma: no cover + if session and not isinstance(session, httpx.Client): # pyright: ignore + raise ValueError("session parameter must be an httpx.Client") + + wire = q.to_wire() + headers = {"accept": "application/dns-message"} + + h1 = http_version in (HTTPVersion.H1, HTTPVersion.DEFAULT) + h2 = http_version in (HTTPVersion.H2, HTTPVersion.DEFAULT) + + # set source port and source address + + if the_source is None: + local_address = None + local_port = 0 + else: + local_address = the_source[0] + local_port = the_source[1] + + if session: + cm: contextlib.AbstractContextManager = contextlib.nullcontext(session) + else: + transport = _HTTPTransport( + local_address=local_address, + http1=h1, + http2=h2, + verify=verify, + local_port=local_port, + bootstrap_address=bootstrap_address, + resolver=resolver, + family=family, # pyright: ignore + ) + + cm = httpx.Client( # type: ignore + http1=h1, http2=h2, verify=verify, transport=transport # type: ignore + ) + with cm as session: + # see https://tools.ietf.org/html/rfc8484#section-4.1.1 for DoH + # GET and POST examples + assert session is not None + if post: + headers.update( + { + "content-type": "application/dns-message", + "content-length": str(len(wire)), + } + ) + response = session.post( + url, + headers=headers, + content=wire, + timeout=timeout, + extensions=extensions, + ) + else: + wire = base64.urlsafe_b64encode(wire).rstrip(b"=") + twire = wire.decode() # httpx does a repr() if we give it bytes + response = session.get( + url, + headers=headers, + timeout=timeout, + params={"dns": twire}, + extensions=extensions, + ) + + # see https://tools.ietf.org/html/rfc8484#section-4.2.1 for info about DoH + # status codes + if response.status_code < 200 or response.status_code > 299: + raise ValueError( + f"{where} responded with status code {response.status_code}" + f"\nResponse body: {response.content}" + ) + r = dns.message.from_wire( + response.content, + keyring=q.keyring, + request_mac=q.request_mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ) + r.time = response.elapsed.total_seconds() + if not q.is_response(r): + raise BadResponse + return r + + +def _find_header(headers: dns.quic.Headers, name: bytes) -> bytes: + if headers is None: + raise KeyError + for header, value in headers: + if header == name: + return value + raise KeyError + + +def _check_status(headers: dns.quic.Headers, peer: str, wire: bytes) -> None: + value = _find_header(headers, b":status") + if value is None: + raise SyntaxError("no :status header in response") + status = int(value) + if status < 0: + raise SyntaxError("status is negative") + if status < 200 or status > 299: + error = "" + if len(wire) > 0: + try: + error = ": " + wire.decode() + except Exception: + pass + raise ValueError(f"{peer} responded with status code {status}{error}") + + +def _http3( + q: dns.message.Message, + where: str, + url: str, + timeout: float | None = None, + port: int = 443, + source: str | None = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + verify: bool | str | ssl.SSLContext = True, + post: bool = True, + connection: dns.quic.SyncQuicConnection | None = None, +) -> dns.message.Message: + if not dns.quic.have_quic: + raise NoDOH("DNS-over-HTTP3 is not available.") # pragma: no cover + + url_parts = urllib.parse.urlparse(url) + hostname = url_parts.hostname + assert hostname is not None + if url_parts.port is not None: + port = url_parts.port + + q.id = 0 + wire = q.to_wire() + the_connection: dns.quic.SyncQuicConnection + the_manager: dns.quic.SyncQuicManager + if connection: + manager: contextlib.AbstractContextManager = contextlib.nullcontext(None) + else: + manager = dns.quic.SyncQuicManager( + verify_mode=verify, server_name=hostname, h3=True # pyright: ignore + ) + the_manager = manager # for type checking happiness + + with manager: + if connection: + the_connection = connection + else: + the_connection = the_manager.connect( # pyright: ignore + where, port, source, source_port + ) + (start, expiration) = _compute_times(timeout) + with the_connection.make_stream(timeout) as stream: # pyright: ignore + stream.send_h3(url, wire, post) + wire = stream.receive(_remaining(expiration)) + _check_status(stream.headers(), where, wire) + finish = time.time() + r = dns.message.from_wire( + wire, + keyring=q.keyring, + request_mac=q.request_mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ) + r.time = max(finish - start, 0.0) + if not q.is_response(r): + raise BadResponse + return r + + +def _udp_recv(sock, max_size, expiration): + """Reads a datagram from the socket. + A Timeout exception will be raised if the operation is not completed + by the expiration time. + """ + while True: + try: + return sock.recvfrom(max_size) + except BlockingIOError: + _wait_for_readable(sock, expiration) + + +def _udp_send(sock, data, destination, expiration): + """Sends the specified datagram to destination over the socket. + A Timeout exception will be raised if the operation is not completed + by the expiration time. + """ + while True: + try: + if destination: + return sock.sendto(data, destination) + else: + return sock.send(data) + except BlockingIOError: # pragma: no cover + _wait_for_writable(sock, expiration) + + +def send_udp( + sock: Any, + what: dns.message.Message | bytes, + destination: Any, + expiration: float | None = None, +) -> Tuple[int, float]: + """Send a DNS message to the specified UDP socket. + + *sock*, a ``socket``. + + *what*, a ``bytes`` or ``dns.message.Message``, the message to send. + + *destination*, a destination tuple appropriate for the address family + of the socket, specifying where to send the query. + + *expiration*, a ``float`` or ``None``, the absolute time at which + a timeout exception should be raised. If ``None``, no timeout will + occur. + + Returns an ``(int, float)`` tuple of bytes sent and the sent time. + """ + + if isinstance(what, dns.message.Message): + what = what.to_wire() + sent_time = time.time() + n = _udp_send(sock, what, destination, expiration) + return (n, sent_time) + + +def receive_udp( + sock: Any, + destination: Any | None = None, + expiration: float | None = None, + ignore_unexpected: bool = False, + one_rr_per_rrset: bool = False, + keyring: Dict[dns.name.Name, dns.tsig.Key] | None = None, + request_mac: bytes | None = b"", + ignore_trailing: bool = False, + raise_on_truncation: bool = False, + ignore_errors: bool = False, + query: dns.message.Message | None = None, +) -> Any: + """Read a DNS message from a UDP socket. + + *sock*, a ``socket``. + + *destination*, a destination tuple appropriate for the address family + of the socket, specifying where the message is expected to arrive from. + When receiving a response, this would be where the associated query was + sent. + + *expiration*, a ``float`` or ``None``, the absolute time at which + a timeout exception should be raised. If ``None``, no timeout will + occur. + + *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from + unexpected sources. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own + RRset. + + *keyring*, a ``dict``, the keyring to use for TSIG. + + *request_mac*, a ``bytes`` or ``None``, the MAC of the request (for TSIG). + + *ignore_trailing*, a ``bool``. If ``True``, ignore trailing + junk at end of the received message. + + *raise_on_truncation*, a ``bool``. If ``True``, raise an exception if + the TC bit is set. + + Raises if the message is malformed, if network errors occur, of if + there is a timeout. + + If *destination* is not ``None``, returns a ``(dns.message.Message, float)`` + tuple of the received message and the received time. + + If *destination* is ``None``, returns a + ``(dns.message.Message, float, tuple)`` + tuple of the received message, the received time, and the address where + the message arrived from. + + *ignore_errors*, a ``bool``. If various format errors or response + mismatches occur, ignore them and keep listening for a valid response. + The default is ``False``. + + *query*, a ``dns.message.Message`` or ``None``. If not ``None`` and + *ignore_errors* is ``True``, check that the received message is a response + to this query, and if not keep listening for a valid response. + """ + + wire = b"" + while True: + (wire, from_address) = _udp_recv(sock, 65535, expiration) + if not _matches_destination( + sock.family, from_address, destination, ignore_unexpected + ): + continue + received_time = time.time() + try: + r = dns.message.from_wire( + wire, + keyring=keyring, + request_mac=request_mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + raise_on_truncation=raise_on_truncation, + ) + except dns.message.Truncated as e: + # If we got Truncated and not FORMERR, we at least got the header with TC + # set, and very likely the question section, so we'll re-raise if the + # message seems to be a response as we need to know when truncation happens. + # We need to check that it seems to be a response as we don't want a random + # injected message with TC set to cause us to bail out. + if ( + ignore_errors + and query is not None + and not query.is_response(e.message()) + ): + continue + else: + raise + except Exception: + if ignore_errors: + continue + else: + raise + if ignore_errors and query is not None and not query.is_response(r): + continue + if destination: + return (r, received_time) + else: + return (r, received_time, from_address) + + +def udp( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 53, + source: str | None = None, + source_port: int = 0, + ignore_unexpected: bool = False, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + raise_on_truncation: bool = False, + sock: Any | None = None, + ignore_errors: bool = False, +) -> dns.message.Message: + """Return the response obtained after sending a query via UDP. + + *q*, a ``dns.message.Message``, the query to send + + *where*, a ``str`` containing an IPv4 or IPv6 address, where + to send the message. + + *timeout*, a ``float`` or ``None``, the number of seconds to wait before the + query times out. If ``None``, the default, wait forever. + + *port*, an ``int``, the port send the message to. The default is 53. + + *source*, a ``str`` containing an IPv4 or IPv6 address, specifying + the source address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. + The default is 0. + + *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from + unexpected sources. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own + RRset. + + *ignore_trailing*, a ``bool``. If ``True``, ignore trailing + junk at end of the received message. + + *raise_on_truncation*, a ``bool``. If ``True``, raise an exception if + the TC bit is set. + + *sock*, a ``socket.socket``, or ``None``, the socket to use for the + query. If ``None``, the default, a socket is created. Note that + if a socket is provided, it must be a nonblocking datagram socket, + and the *source* and *source_port* are ignored. + + *ignore_errors*, a ``bool``. If various format errors or response + mismatches occur, ignore them and keep listening for a valid response. + The default is ``False``. + + Returns a ``dns.message.Message``. + """ + + wire = q.to_wire() + (af, destination, source) = _destination_and_source( + where, port, source, source_port, True + ) + (begin_time, expiration) = _compute_times(timeout) + if sock: + cm: contextlib.AbstractContextManager = contextlib.nullcontext(sock) + else: + assert af is not None + cm = make_socket(af, socket.SOCK_DGRAM, source) + with cm as s: + send_udp(s, wire, destination, expiration) + (r, received_time) = receive_udp( + s, + destination, + expiration, + ignore_unexpected, + one_rr_per_rrset, + q.keyring, + q.mac, + ignore_trailing, + raise_on_truncation, + ignore_errors, + q, + ) + r.time = received_time - begin_time + # We don't need to check q.is_response() if we are in ignore_errors mode + # as receive_udp() will have checked it. + if not (ignore_errors or q.is_response(r)): + raise BadResponse + return r + assert ( + False # help mypy figure out we can't get here lgtm[py/unreachable-statement] + ) + + +def udp_with_fallback( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 53, + source: str | None = None, + source_port: int = 0, + ignore_unexpected: bool = False, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + udp_sock: Any | None = None, + tcp_sock: Any | None = None, + ignore_errors: bool = False, +) -> Tuple[dns.message.Message, bool]: + """Return the response to the query, trying UDP first and falling back + to TCP if UDP results in a truncated response. + + *q*, a ``dns.message.Message``, the query to send + + *where*, a ``str`` containing an IPv4 or IPv6 address, where to send the message. + + *timeout*, a ``float`` or ``None``, the number of seconds to wait before the query + times out. If ``None``, the default, wait forever. + + *port*, an ``int``, the port send the message to. The default is 53. + + *source*, a ``str`` containing an IPv4 or IPv6 address, specifying the source + address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. The default is + 0. + + *ignore_unexpected*, a ``bool``. If ``True``, ignore responses from unexpected + sources. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own RRset. + + *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the + received message. + + *udp_sock*, a ``socket.socket``, or ``None``, the socket to use for the UDP query. + If ``None``, the default, a socket is created. Note that if a socket is provided, + it must be a nonblocking datagram socket, and the *source* and *source_port* are + ignored for the UDP query. + + *tcp_sock*, a ``socket.socket``, or ``None``, the connected socket to use for the + TCP query. If ``None``, the default, a socket is created. Note that if a socket is + provided, it must be a nonblocking connected stream socket, and *where*, *source* + and *source_port* are ignored for the TCP query. + + *ignore_errors*, a ``bool``. If various format errors or response mismatches occur + while listening for UDP, ignore them and keep listening for a valid response. The + default is ``False``. + + Returns a (``dns.message.Message``, tcp) tuple where tcp is ``True`` if and only if + TCP was used. + """ + try: + response = udp( + q, + where, + timeout, + port, + source, + source_port, + ignore_unexpected, + one_rr_per_rrset, + ignore_trailing, + True, + udp_sock, + ignore_errors, + ) + return (response, False) + except dns.message.Truncated: + response = tcp( + q, + where, + timeout, + port, + source, + source_port, + one_rr_per_rrset, + ignore_trailing, + tcp_sock, + ) + return (response, True) + + +def _net_read(sock, count, expiration): + """Read the specified number of bytes from sock. Keep trying until we + either get the desired amount, or we hit EOF. + A Timeout exception will be raised if the operation is not completed + by the expiration time. + """ + s = b"" + while count > 0: + try: + n = sock.recv(count) + if n == b"": + raise EOFError("EOF") + count -= len(n) + s += n + except (BlockingIOError, ssl.SSLWantReadError): + _wait_for_readable(sock, expiration) + except ssl.SSLWantWriteError: # pragma: no cover + _wait_for_writable(sock, expiration) + return s + + +def _net_write(sock, data, expiration): + """Write the specified data to the socket. + A Timeout exception will be raised if the operation is not completed + by the expiration time. + """ + current = 0 + l = len(data) + while current < l: + try: + current += sock.send(data[current:]) + except (BlockingIOError, ssl.SSLWantWriteError): + _wait_for_writable(sock, expiration) + except ssl.SSLWantReadError: # pragma: no cover + _wait_for_readable(sock, expiration) + + +def send_tcp( + sock: Any, + what: dns.message.Message | bytes, + expiration: float | None = None, +) -> Tuple[int, float]: + """Send a DNS message to the specified TCP socket. + + *sock*, a ``socket``. + + *what*, a ``bytes`` or ``dns.message.Message``, the message to send. + + *expiration*, a ``float`` or ``None``, the absolute time at which + a timeout exception should be raised. If ``None``, no timeout will + occur. + + Returns an ``(int, float)`` tuple of bytes sent and the sent time. + """ + + if isinstance(what, dns.message.Message): + tcpmsg = what.to_wire(prepend_length=True) + else: + # copying the wire into tcpmsg is inefficient, but lets us + # avoid writev() or doing a short write that would get pushed + # onto the net + tcpmsg = len(what).to_bytes(2, "big") + what + sent_time = time.time() + _net_write(sock, tcpmsg, expiration) + return (len(tcpmsg), sent_time) + + +def receive_tcp( + sock: Any, + expiration: float | None = None, + one_rr_per_rrset: bool = False, + keyring: Dict[dns.name.Name, dns.tsig.Key] | None = None, + request_mac: bytes | None = b"", + ignore_trailing: bool = False, +) -> Tuple[dns.message.Message, float]: + """Read a DNS message from a TCP socket. + + *sock*, a ``socket``. + + *expiration*, a ``float`` or ``None``, the absolute time at which + a timeout exception should be raised. If ``None``, no timeout will + occur. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own + RRset. + + *keyring*, a ``dict``, the keyring to use for TSIG. + + *request_mac*, a ``bytes`` or ``None``, the MAC of the request (for TSIG). + + *ignore_trailing*, a ``bool``. If ``True``, ignore trailing + junk at end of the received message. + + Raises if the message is malformed, if network errors occur, of if + there is a timeout. + + Returns a ``(dns.message.Message, float)`` tuple of the received message + and the received time. + """ + + ldata = _net_read(sock, 2, expiration) + (l,) = struct.unpack("!H", ldata) + wire = _net_read(sock, l, expiration) + received_time = time.time() + r = dns.message.from_wire( + wire, + keyring=keyring, + request_mac=request_mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ) + return (r, received_time) + + +def _connect(s, address, expiration): + err = s.connect_ex(address) + if err == 0: + return + if err in (errno.EINPROGRESS, errno.EWOULDBLOCK, errno.EALREADY): + _wait_for_writable(s, expiration) + err = s.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if err != 0: + raise OSError(err, os.strerror(err)) + + +def tcp( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 53, + source: str | None = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + sock: Any | None = None, +) -> dns.message.Message: + """Return the response obtained after sending a query via TCP. + + *q*, a ``dns.message.Message``, the query to send + + *where*, a ``str`` containing an IPv4 or IPv6 address, where + to send the message. + + *timeout*, a ``float`` or ``None``, the number of seconds to wait before the + query times out. If ``None``, the default, wait forever. + + *port*, an ``int``, the port send the message to. The default is 53. + + *source*, a ``str`` containing an IPv4 or IPv6 address, specifying + the source address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. + The default is 0. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own + RRset. + + *ignore_trailing*, a ``bool``. If ``True``, ignore trailing + junk at end of the received message. + + *sock*, a ``socket.socket``, or ``None``, the connected socket to use for the + query. If ``None``, the default, a socket is created. Note that + if a socket is provided, it must be a nonblocking connected stream + socket, and *where*, *port*, *source* and *source_port* are ignored. + + Returns a ``dns.message.Message``. + """ + + wire = q.to_wire() + (begin_time, expiration) = _compute_times(timeout) + if sock: + cm: contextlib.AbstractContextManager = contextlib.nullcontext(sock) + else: + (af, destination, source) = _destination_and_source( + where, port, source, source_port, True + ) + assert af is not None + cm = make_socket(af, socket.SOCK_STREAM, source) + with cm as s: + if not sock: + # pylint: disable=possibly-used-before-assignment + _connect(s, destination, expiration) # pyright: ignore + send_tcp(s, wire, expiration) + (r, received_time) = receive_tcp( + s, expiration, one_rr_per_rrset, q.keyring, q.mac, ignore_trailing + ) + r.time = received_time - begin_time + if not q.is_response(r): + raise BadResponse + return r + assert ( + False # help mypy figure out we can't get here lgtm[py/unreachable-statement] + ) + + +def _tls_handshake(s, expiration): + while True: + try: + s.do_handshake() + return + except ssl.SSLWantReadError: + _wait_for_readable(s, expiration) + except ssl.SSLWantWriteError: # pragma: no cover + _wait_for_writable(s, expiration) + + +def make_ssl_context( + verify: bool | str = True, + check_hostname: bool = True, + alpns: list[str] | None = None, +) -> ssl.SSLContext: + """Make an SSL context + + If *verify* is ``True``, the default, then certificate verification will occur using + the standard CA roots. If *verify* is ``False``, then certificate verification will + be disabled. If *verify* is a string which is a valid pathname, then if the + pathname is a regular file, the CA roots will be taken from the file, otherwise if + the pathname is a directory roots will be taken from the directory. + + If *check_hostname* is ``True``, the default, then the hostname of the server must + be specified when connecting and the server's certificate must authorize the + hostname. If ``False``, then hostname checking is disabled. + + *aplns* is ``None`` or a list of TLS ALPN (Application Layer Protocol Negotiation) + strings to use in negotiation. For DNS-over-TLS, the right value is `["dot"]`. + """ + cafile, capath = dns._tls_util.convert_verify_to_cafile_and_capath(verify) + ssl_context = ssl.create_default_context(cafile=cafile, capath=capath) + # the pyright ignores below are because it gets confused between the + # _no_ssl compatibility types and the real ones. + ssl_context.minimum_version = ssl.TLSVersion.TLSv1_2 # type: ignore + ssl_context.check_hostname = check_hostname + if verify is False: + ssl_context.verify_mode = ssl.CERT_NONE # type: ignore + if alpns is not None: + ssl_context.set_alpn_protocols(alpns) + return ssl_context # type: ignore + + +# for backwards compatibility +def _make_dot_ssl_context( + server_hostname: str | None, verify: bool | str +) -> ssl.SSLContext: + return make_ssl_context(verify, server_hostname is not None, ["dot"]) + + +def tls( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 853, + source: str | None = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + sock: ssl.SSLSocket | None = None, + ssl_context: ssl.SSLContext | None = None, + server_hostname: str | None = None, + verify: bool | str = True, +) -> dns.message.Message: + """Return the response obtained after sending a query via TLS. + + *q*, a ``dns.message.Message``, the query to send + + *where*, a ``str`` containing an IPv4 or IPv6 address, where + to send the message. + + *timeout*, a ``float`` or ``None``, the number of seconds to wait before the + query times out. If ``None``, the default, wait forever. + + *port*, an ``int``, the port send the message to. The default is 853. + + *source*, a ``str`` containing an IPv4 or IPv6 address, specifying + the source address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. + The default is 0. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own + RRset. + + *ignore_trailing*, a ``bool``. If ``True``, ignore trailing + junk at end of the received message. + + *sock*, an ``ssl.SSLSocket``, or ``None``, the socket to use for + the query. If ``None``, the default, a socket is created. Note + that if a socket is provided, it must be a nonblocking connected + SSL stream socket, and *where*, *port*, *source*, *source_port*, + and *ssl_context* are ignored. + + *ssl_context*, an ``ssl.SSLContext``, the context to use when establishing + a TLS connection. If ``None``, the default, creates one with the default + configuration. + + *server_hostname*, a ``str`` containing the server's hostname. The + default is ``None``, which means that no hostname is known, and if an + SSL context is created, hostname checking will be disabled. + + *verify*, a ``bool`` or ``str``. If a ``True``, then TLS certificate verification + of the server is done using the default CA bundle; if ``False``, then no + verification is done; if a `str` then it specifies the path to a certificate file or + directory which will be used for verification. + + Returns a ``dns.message.Message``. + + """ + + if sock: + # + # If a socket was provided, there's no special TLS handling needed. + # + return tcp( + q, + where, + timeout, + port, + source, + source_port, + one_rr_per_rrset, + ignore_trailing, + sock, + ) + + wire = q.to_wire() + (begin_time, expiration) = _compute_times(timeout) + (af, destination, source) = _destination_and_source( + where, port, source, source_port, True + ) + assert af is not None # where must be an address + if ssl_context is None: + ssl_context = make_ssl_context(verify, server_hostname is not None, ["dot"]) + + with make_ssl_socket( + af, + socket.SOCK_STREAM, + ssl_context=ssl_context, + server_hostname=server_hostname, + source=source, + ) as s: + _connect(s, destination, expiration) + _tls_handshake(s, expiration) + send_tcp(s, wire, expiration) + (r, received_time) = receive_tcp( + s, expiration, one_rr_per_rrset, q.keyring, q.mac, ignore_trailing + ) + r.time = received_time - begin_time + if not q.is_response(r): + raise BadResponse + return r + assert ( + False # help mypy figure out we can't get here lgtm[py/unreachable-statement] + ) + + +def quic( + q: dns.message.Message, + where: str, + timeout: float | None = None, + port: int = 853, + source: str | None = None, + source_port: int = 0, + one_rr_per_rrset: bool = False, + ignore_trailing: bool = False, + connection: dns.quic.SyncQuicConnection | None = None, + verify: bool | str = True, + hostname: str | None = None, + server_hostname: str | None = None, +) -> dns.message.Message: + """Return the response obtained after sending a query via DNS-over-QUIC. + + *q*, a ``dns.message.Message``, the query to send. + + *where*, a ``str``, the nameserver IP address. + + *timeout*, a ``float`` or ``None``, the number of seconds to wait before the query + times out. If ``None``, the default, wait forever. + + *port*, a ``int``, the port to send the query to. The default is 853. + + *source*, a ``str`` containing an IPv4 or IPv6 address, specifying the source + address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. The default is + 0. + + *one_rr_per_rrset*, a ``bool``. If ``True``, put each RR into its own RRset. + + *ignore_trailing*, a ``bool``. If ``True``, ignore trailing junk at end of the + received message. + + *connection*, a ``dns.quic.SyncQuicConnection``. If provided, the connection to use + to send the query. + + *verify*, a ``bool`` or ``str``. If a ``True``, then TLS certificate verification + of the server is done using the default CA bundle; if ``False``, then no + verification is done; if a `str` then it specifies the path to a certificate file or + directory which will be used for verification. + + *hostname*, a ``str`` containing the server's hostname or ``None``. The default is + ``None``, which means that no hostname is known, and if an SSL context is created, + hostname checking will be disabled. This value is ignored if *url* is not + ``None``. + + *server_hostname*, a ``str`` or ``None``. This item is for backwards compatibility + only, and has the same meaning as *hostname*. + + Returns a ``dns.message.Message``. + """ + + if not dns.quic.have_quic: + raise NoDOQ("DNS-over-QUIC is not available.") # pragma: no cover + + if server_hostname is not None and hostname is None: + hostname = server_hostname + + q.id = 0 + wire = q.to_wire() + the_connection: dns.quic.SyncQuicConnection + the_manager: dns.quic.SyncQuicManager + if connection: + manager: contextlib.AbstractContextManager = contextlib.nullcontext(None) + the_connection = connection + else: + manager = dns.quic.SyncQuicManager( + verify_mode=verify, server_name=hostname # pyright: ignore + ) + the_manager = manager # for type checking happiness + + with manager: + if not connection: + the_connection = the_manager.connect( # pyright: ignore + where, port, source, source_port + ) + (start, expiration) = _compute_times(timeout) + with the_connection.make_stream(timeout) as stream: # pyright: ignore + stream.send(wire, True) + wire = stream.receive(_remaining(expiration)) + finish = time.time() + r = dns.message.from_wire( + wire, + keyring=q.keyring, + request_mac=q.request_mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + ) + r.time = max(finish - start, 0.0) + if not q.is_response(r): + raise BadResponse + return r + + +class UDPMode(enum.IntEnum): + """How should UDP be used in an IXFR from :py:func:`inbound_xfr()`? + + NEVER means "never use UDP; always use TCP" + TRY_FIRST means "try to use UDP but fall back to TCP if needed" + ONLY means "raise ``dns.xfr.UseTCP`` if trying UDP does not succeed" + """ + + NEVER = 0 + TRY_FIRST = 1 + ONLY = 2 + + +def _inbound_xfr( + txn_manager: dns.transaction.TransactionManager, + s: socket.socket | ssl.SSLSocket, + query: dns.message.Message, + serial: int | None, + timeout: float | None, + expiration: float | None, +) -> Any: + """Given a socket, does the zone transfer.""" + rdtype = query.question[0].rdtype + is_ixfr = rdtype == dns.rdatatype.IXFR + origin = txn_manager.from_wire_origin() + wire = query.to_wire() + is_udp = isinstance(s, socket.socket) and s.type == socket.SOCK_DGRAM + if is_udp: + _udp_send(s, wire, None, expiration) + else: + tcpmsg = struct.pack("!H", len(wire)) + wire + _net_write(s, tcpmsg, expiration) + with dns.xfr.Inbound(txn_manager, rdtype, serial, is_udp) as inbound: + done = False + tsig_ctx = None + r: dns.message.Message | None = None + while not done: + (_, mexpiration) = _compute_times(timeout) + if mexpiration is None or ( + expiration is not None and mexpiration > expiration + ): + mexpiration = expiration + if is_udp: + (rwire, _) = _udp_recv(s, 65535, mexpiration) + else: + ldata = _net_read(s, 2, mexpiration) + (l,) = struct.unpack("!H", ldata) + rwire = _net_read(s, l, mexpiration) + r = dns.message.from_wire( + rwire, + keyring=query.keyring, + request_mac=query.mac, + xfr=True, + origin=origin, + tsig_ctx=tsig_ctx, + multi=(not is_udp), + one_rr_per_rrset=is_ixfr, + ) + done = inbound.process_message(r) + yield r + tsig_ctx = r.tsig_ctx + if query.keyring and r is not None and not r.had_tsig: + raise dns.exception.FormError("missing TSIG") + + +def xfr( + where: str, + zone: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.AXFR, + rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + timeout: float | None = None, + port: int = 53, + keyring: Dict[dns.name.Name, dns.tsig.Key] | None = None, + keyname: dns.name.Name | str | None = None, + relativize: bool = True, + lifetime: float | None = None, + source: str | None = None, + source_port: int = 0, + serial: int = 0, + use_udp: bool = False, + keyalgorithm: dns.name.Name | str = dns.tsig.default_algorithm, +) -> Any: + """Return a generator for the responses to a zone transfer. + + *where*, a ``str`` containing an IPv4 or IPv6 address, where + to send the message. + + *zone*, a ``dns.name.Name`` or ``str``, the name of the zone to transfer. + + *rdtype*, an ``int`` or ``str``, the type of zone transfer. The + default is ``dns.rdatatype.AXFR``. ``dns.rdatatype.IXFR`` can be + used to do an incremental transfer instead. + + *rdclass*, an ``int`` or ``str``, the class of the zone transfer. + The default is ``dns.rdataclass.IN``. + + *timeout*, a ``float``, the number of seconds to wait for each + response message. If None, the default, wait forever. + + *port*, an ``int``, the port send the message to. The default is 53. + + *keyring*, a ``dict``, the keyring to use for TSIG. + + *keyname*, a ``dns.name.Name`` or ``str``, the name of the TSIG + key to use. + + *relativize*, a ``bool``. If ``True``, all names in the zone will be + relativized to the zone origin. It is essential that the + relativize setting matches the one specified to + ``dns.zone.from_xfr()`` if using this generator to make a zone. + + *lifetime*, a ``float``, the total number of seconds to spend + doing the transfer. If ``None``, the default, then there is no + limit on the time the transfer may take. + + *source*, a ``str`` containing an IPv4 or IPv6 address, specifying + the source address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. + The default is 0. + + *serial*, an ``int``, the SOA serial number to use as the base for + an IXFR diff sequence (only meaningful if *rdtype* is + ``dns.rdatatype.IXFR``). + + *use_udp*, a ``bool``. If ``True``, use UDP (only meaningful for IXFR). + + *keyalgorithm*, a ``dns.name.Name`` or ``str``, the TSIG algorithm to use. + + Raises on errors, and so does the generator. + + Returns a generator of ``dns.message.Message`` objects. + """ + + class DummyTransactionManager(dns.transaction.TransactionManager): + def __init__(self, origin, relativize): + self.info = (origin, relativize, dns.name.empty if relativize else origin) + + def origin_information(self): + return self.info + + def get_class(self) -> dns.rdataclass.RdataClass: + raise NotImplementedError # pragma: no cover + + def reader(self): + raise NotImplementedError # pragma: no cover + + def writer(self, replacement: bool = False) -> dns.transaction.Transaction: + class DummyTransaction: + def nop(self, *args, **kw): + pass + + def __getattr__(self, _): + return self.nop + + return cast(dns.transaction.Transaction, DummyTransaction()) + + if isinstance(zone, str): + zone = dns.name.from_text(zone) + rdtype = dns.rdatatype.RdataType.make(rdtype) + q = dns.message.make_query(zone, rdtype, rdclass) + if rdtype == dns.rdatatype.IXFR: + rrset = q.find_rrset( + q.authority, zone, dns.rdataclass.IN, dns.rdatatype.SOA, create=True + ) + soa = dns.rdata.from_text("IN", "SOA", f". . {serial} 0 0 0 0") + rrset.add(soa, 0) + if keyring is not None: + q.use_tsig(keyring, keyname, algorithm=keyalgorithm) + (af, destination, source) = _destination_and_source( + where, port, source, source_port, True + ) + assert af is not None + (_, expiration) = _compute_times(lifetime) + tm = DummyTransactionManager(zone, relativize) + if use_udp and rdtype != dns.rdatatype.IXFR: + raise ValueError("cannot do a UDP AXFR") + sock_type = socket.SOCK_DGRAM if use_udp else socket.SOCK_STREAM + with make_socket(af, sock_type, source) as s: + _connect(s, destination, expiration) + yield from _inbound_xfr(tm, s, q, serial, timeout, expiration) + + +def inbound_xfr( + where: str, + txn_manager: dns.transaction.TransactionManager, + query: dns.message.Message | None = None, + port: int = 53, + timeout: float | None = None, + lifetime: float | None = None, + source: str | None = None, + source_port: int = 0, + udp_mode: UDPMode = UDPMode.NEVER, +) -> None: + """Conduct an inbound transfer and apply it via a transaction from the + txn_manager. + + *where*, a ``str`` containing an IPv4 or IPv6 address, where + to send the message. + + *txn_manager*, a ``dns.transaction.TransactionManager``, the txn_manager + for this transfer (typically a ``dns.zone.Zone``). + + *query*, the query to send. If not supplied, a default query is + constructed using information from the *txn_manager*. + + *port*, an ``int``, the port send the message to. The default is 53. + + *timeout*, a ``float``, the number of seconds to wait for each + response message. If None, the default, wait forever. + + *lifetime*, a ``float``, the total number of seconds to spend + doing the transfer. If ``None``, the default, then there is no + limit on the time the transfer may take. + + *source*, a ``str`` containing an IPv4 or IPv6 address, specifying + the source address. The default is the wildcard address. + + *source_port*, an ``int``, the port from which to send the message. + The default is 0. + + *udp_mode*, a ``dns.query.UDPMode``, determines how UDP is used + for IXFRs. The default is ``dns.query.UDPMode.NEVER``, i.e. only use + TCP. Other possibilities are ``dns.query.UDPMode.TRY_FIRST``, which + means "try UDP but fallback to TCP if needed", and + ``dns.query.UDPMode.ONLY``, which means "try UDP and raise + ``dns.xfr.UseTCP`` if it does not succeed. + + Raises on errors. + """ + if query is None: + (query, serial) = dns.xfr.make_query(txn_manager) + else: + serial = dns.xfr.extract_serial_from_query(query) + + (af, destination, source) = _destination_and_source( + where, port, source, source_port, True + ) + assert af is not None + (_, expiration) = _compute_times(lifetime) + if query.question[0].rdtype == dns.rdatatype.IXFR and udp_mode != UDPMode.NEVER: + with make_socket(af, socket.SOCK_DGRAM, source) as s: + _connect(s, destination, expiration) + try: + for _ in _inbound_xfr( + txn_manager, s, query, serial, timeout, expiration + ): + pass + return + except dns.xfr.UseTCP: + if udp_mode == UDPMode.ONLY: + raise + + with make_socket(af, socket.SOCK_STREAM, source) as s: + _connect(s, destination, expiration) + for _ in _inbound_xfr(txn_manager, s, query, serial, timeout, expiration): + pass diff --git a/netdeploy/lib/python3.11/site-packages/dns/quic/__init__.py b/netdeploy/lib/python3.11/site-packages/dns/quic/__init__.py new file mode 100644 index 0000000..7c2a699 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/quic/__init__.py @@ -0,0 +1,78 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +from typing import Any, Dict, List, Tuple + +import dns._features +import dns.asyncbackend + +if dns._features.have("doq"): + from dns._asyncbackend import NullContext + from dns.quic._asyncio import AsyncioQuicConnection as AsyncioQuicConnection + from dns.quic._asyncio import AsyncioQuicManager + from dns.quic._asyncio import AsyncioQuicStream as AsyncioQuicStream + from dns.quic._common import AsyncQuicConnection # pyright: ignore + from dns.quic._common import AsyncQuicManager as AsyncQuicManager + from dns.quic._sync import SyncQuicConnection # pyright: ignore + from dns.quic._sync import SyncQuicStream # pyright: ignore + from dns.quic._sync import SyncQuicManager as SyncQuicManager + + have_quic = True + + def null_factory( + *args, # pylint: disable=unused-argument + **kwargs, # pylint: disable=unused-argument + ): + return NullContext(None) + + def _asyncio_manager_factory( + context, *args, **kwargs # pylint: disable=unused-argument + ): + return AsyncioQuicManager(*args, **kwargs) + + # We have a context factory and a manager factory as for trio we need to have + # a nursery. + + _async_factories: Dict[str, Tuple[Any, Any]] = { + "asyncio": (null_factory, _asyncio_manager_factory) + } + + if dns._features.have("trio"): + import trio + + # pylint: disable=ungrouped-imports + from dns.quic._trio import TrioQuicConnection as TrioQuicConnection + from dns.quic._trio import TrioQuicManager + from dns.quic._trio import TrioQuicStream as TrioQuicStream + + def _trio_context_factory(): + return trio.open_nursery() + + def _trio_manager_factory(context, *args, **kwargs): + return TrioQuicManager(context, *args, **kwargs) + + _async_factories["trio"] = (_trio_context_factory, _trio_manager_factory) + + def factories_for_backend(backend=None): + if backend is None: + backend = dns.asyncbackend.get_default_backend() + return _async_factories[backend.name()] + +else: # pragma: no cover + have_quic = False + + class AsyncQuicStream: # type: ignore + pass + + class AsyncQuicConnection: # type: ignore + async def make_stream(self) -> Any: + raise NotImplementedError + + class SyncQuicStream: # type: ignore + pass + + class SyncQuicConnection: # type: ignore + def make_stream(self) -> Any: + raise NotImplementedError + + +Headers = List[Tuple[bytes, bytes]] diff --git a/netdeploy/lib/python3.11/site-packages/dns/quic/_asyncio.py b/netdeploy/lib/python3.11/site-packages/dns/quic/_asyncio.py new file mode 100644 index 0000000..0a177b6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/quic/_asyncio.py @@ -0,0 +1,276 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import asyncio +import socket +import ssl +import struct +import time + +import aioquic.h3.connection # type: ignore +import aioquic.h3.events # type: ignore +import aioquic.quic.configuration # type: ignore +import aioquic.quic.connection # type: ignore +import aioquic.quic.events # type: ignore + +import dns.asyncbackend +import dns.exception +import dns.inet +from dns.quic._common import ( + QUIC_MAX_DATAGRAM, + AsyncQuicConnection, + AsyncQuicManager, + BaseQuicStream, + UnexpectedEOF, +) + + +class AsyncioQuicStream(BaseQuicStream): + def __init__(self, connection, stream_id): + super().__init__(connection, stream_id) + self._wake_up = asyncio.Condition() + + async def _wait_for_wake_up(self): + async with self._wake_up: + await self._wake_up.wait() + + async def wait_for(self, amount, expiration): + while True: + timeout = self._timeout_from_expiration(expiration) + if self._buffer.have(amount): + return + self._expecting = amount + try: + await asyncio.wait_for(self._wait_for_wake_up(), timeout) + except TimeoutError: + raise dns.exception.Timeout + self._expecting = 0 + + async def wait_for_end(self, expiration): + while True: + timeout = self._timeout_from_expiration(expiration) + if self._buffer.seen_end(): + return + try: + await asyncio.wait_for(self._wait_for_wake_up(), timeout) + except TimeoutError: + raise dns.exception.Timeout + + async def receive(self, timeout=None): + expiration = self._expiration_from_timeout(timeout) + if self._connection.is_h3(): + await self.wait_for_end(expiration) + return self._buffer.get_all() + else: + await self.wait_for(2, expiration) + (size,) = struct.unpack("!H", self._buffer.get(2)) + await self.wait_for(size, expiration) + return self._buffer.get(size) + + async def send(self, datagram, is_end=False): + data = self._encapsulate(datagram) + await self._connection.write(self._stream_id, data, is_end) + + async def _add_input(self, data, is_end): + if self._common_add_input(data, is_end): + async with self._wake_up: + self._wake_up.notify() + + async def close(self): + self._close() + + # Streams are async context managers + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() + async with self._wake_up: + self._wake_up.notify() + return False + + +class AsyncioQuicConnection(AsyncQuicConnection): + def __init__(self, connection, address, port, source, source_port, manager=None): + super().__init__(connection, address, port, source, source_port, manager) + self._socket = None + self._handshake_complete = asyncio.Event() + self._socket_created = asyncio.Event() + self._wake_timer = asyncio.Condition() + self._receiver_task = None + self._sender_task = None + self._wake_pending = False + + async def _receiver(self): + try: + af = dns.inet.af_for_address(self._address) + backend = dns.asyncbackend.get_backend("asyncio") + # Note that peer is a low-level address tuple, but make_socket() wants + # a high-level address tuple, so we convert. + self._socket = await backend.make_socket( + af, socket.SOCK_DGRAM, 0, self._source, (self._peer[0], self._peer[1]) + ) + self._socket_created.set() + async with self._socket: + while not self._done: + (datagram, address) = await self._socket.recvfrom( + QUIC_MAX_DATAGRAM, None + ) + if address[0] != self._peer[0] or address[1] != self._peer[1]: + continue + self._connection.receive_datagram(datagram, address, time.time()) + # Wake up the timer in case the sender is sleeping, as there may be + # stuff to send now. + await self._wakeup() + except Exception: + pass + finally: + self._done = True + await self._wakeup() + self._handshake_complete.set() + + async def _wakeup(self): + self._wake_pending = True + async with self._wake_timer: + self._wake_timer.notify_all() + + async def _wait_for_wake_timer(self): + async with self._wake_timer: + if not self._wake_pending: + await self._wake_timer.wait() + self._wake_pending = False + + async def _sender(self): + await self._socket_created.wait() + while not self._done: + datagrams = self._connection.datagrams_to_send(time.time()) + for datagram, address in datagrams: + assert address == self._peer + assert self._socket is not None + await self._socket.sendto(datagram, self._peer, None) + (expiration, interval) = self._get_timer_values() + try: + await asyncio.wait_for(self._wait_for_wake_timer(), interval) + except Exception: + pass + self._handle_timer(expiration) + await self._handle_events() + + async def _handle_events(self): + count = 0 + while True: + event = self._connection.next_event() + if event is None: + return + if isinstance(event, aioquic.quic.events.StreamDataReceived): + if self.is_h3(): + assert self._h3_conn is not None + h3_events = self._h3_conn.handle_event(event) + for h3_event in h3_events: + if isinstance(h3_event, aioquic.h3.events.HeadersReceived): + stream = self._streams.get(event.stream_id) + if stream: + if stream._headers is None: + stream._headers = h3_event.headers + elif stream._trailers is None: + stream._trailers = h3_event.headers + if h3_event.stream_ended: + await stream._add_input(b"", True) + elif isinstance(h3_event, aioquic.h3.events.DataReceived): + stream = self._streams.get(event.stream_id) + if stream: + await stream._add_input( + h3_event.data, h3_event.stream_ended + ) + else: + stream = self._streams.get(event.stream_id) + if stream: + await stream._add_input(event.data, event.end_stream) + elif isinstance(event, aioquic.quic.events.HandshakeCompleted): + self._handshake_complete.set() + elif isinstance(event, aioquic.quic.events.ConnectionTerminated): + self._done = True + if self._receiver_task is not None: + self._receiver_task.cancel() + elif isinstance(event, aioquic.quic.events.StreamReset): + stream = self._streams.get(event.stream_id) + if stream: + await stream._add_input(b"", True) + + count += 1 + if count > 10: + # yield + count = 0 + await asyncio.sleep(0) + + async def write(self, stream, data, is_end=False): + self._connection.send_stream_data(stream, data, is_end) + await self._wakeup() + + def run(self): + if self._closed: + return + self._receiver_task = asyncio.Task(self._receiver()) + self._sender_task = asyncio.Task(self._sender()) + + async def make_stream(self, timeout=None): + try: + await asyncio.wait_for(self._handshake_complete.wait(), timeout) + except TimeoutError: + raise dns.exception.Timeout + if self._done: + raise UnexpectedEOF + stream_id = self._connection.get_next_available_stream_id(False) + stream = AsyncioQuicStream(self, stream_id) + self._streams[stream_id] = stream + return stream + + async def close(self): + if not self._closed: + if self._manager is not None: + self._manager.closed(self._peer[0], self._peer[1]) + self._closed = True + self._connection.close() + # sender might be blocked on this, so set it + self._socket_created.set() + await self._wakeup() + try: + if self._receiver_task is not None: + await self._receiver_task + except asyncio.CancelledError: + pass + try: + if self._sender_task is not None: + await self._sender_task + except asyncio.CancelledError: + pass + if self._socket is not None: + await self._socket.close() + + +class AsyncioQuicManager(AsyncQuicManager): + def __init__( + self, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None, h3=False + ): + super().__init__(conf, verify_mode, AsyncioQuicConnection, server_name, h3) + + def connect( + self, address, port=853, source=None, source_port=0, want_session_ticket=True + ): + (connection, start) = self._connect( + address, port, source, source_port, want_session_ticket + ) + if start: + connection.run() + return connection + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + # Copy the iterator into a list as exiting things will mutate the connections + # table. + connections = list(self._connections.values()) + for connection in connections: + await connection.close() + return False diff --git a/netdeploy/lib/python3.11/site-packages/dns/quic/_common.py b/netdeploy/lib/python3.11/site-packages/dns/quic/_common.py new file mode 100644 index 0000000..ba9d245 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/quic/_common.py @@ -0,0 +1,344 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import base64 +import copy +import functools +import socket +import struct +import time +import urllib.parse +from typing import Any + +import aioquic.h3.connection # type: ignore +import aioquic.quic.configuration # type: ignore +import aioquic.quic.connection # type: ignore + +import dns._tls_util +import dns.inet + +QUIC_MAX_DATAGRAM = 2048 +MAX_SESSION_TICKETS = 8 +# If we hit the max sessions limit we will delete this many of the oldest connections. +# The value must be a integer > 0 and <= MAX_SESSION_TICKETS. +SESSIONS_TO_DELETE = MAX_SESSION_TICKETS // 4 + + +class UnexpectedEOF(Exception): + pass + + +class Buffer: + def __init__(self): + self._buffer = b"" + self._seen_end = False + + def put(self, data, is_end): + if self._seen_end: + return + self._buffer += data + if is_end: + self._seen_end = True + + def have(self, amount): + if len(self._buffer) >= amount: + return True + if self._seen_end: + raise UnexpectedEOF + return False + + def seen_end(self): + return self._seen_end + + def get(self, amount): + assert self.have(amount) + data = self._buffer[:amount] + self._buffer = self._buffer[amount:] + return data + + def get_all(self): + assert self.seen_end() + data = self._buffer + self._buffer = b"" + return data + + +class BaseQuicStream: + def __init__(self, connection, stream_id): + self._connection = connection + self._stream_id = stream_id + self._buffer = Buffer() + self._expecting = 0 + self._headers = None + self._trailers = None + + def id(self): + return self._stream_id + + def headers(self): + return self._headers + + def trailers(self): + return self._trailers + + def _expiration_from_timeout(self, timeout): + if timeout is not None: + expiration = time.time() + timeout + else: + expiration = None + return expiration + + def _timeout_from_expiration(self, expiration): + if expiration is not None: + timeout = max(expiration - time.time(), 0.0) + else: + timeout = None + return timeout + + # Subclass must implement receive() as sync / async and which returns a message + # or raises. + + # Subclass must implement send() as sync / async and which takes a message and + # an EOF indicator. + + def send_h3(self, url, datagram, post=True): + if not self._connection.is_h3(): + raise SyntaxError("cannot send H3 to a non-H3 connection") + url_parts = urllib.parse.urlparse(url) + path = url_parts.path.encode() + if post: + method = b"POST" + else: + method = b"GET" + path += b"?dns=" + base64.urlsafe_b64encode(datagram).rstrip(b"=") + headers = [ + (b":method", method), + (b":scheme", url_parts.scheme.encode()), + (b":authority", url_parts.netloc.encode()), + (b":path", path), + (b"accept", b"application/dns-message"), + ] + if post: + headers.extend( + [ + (b"content-type", b"application/dns-message"), + (b"content-length", str(len(datagram)).encode()), + ] + ) + self._connection.send_headers(self._stream_id, headers, not post) + if post: + self._connection.send_data(self._stream_id, datagram, True) + + def _encapsulate(self, datagram): + if self._connection.is_h3(): + return datagram + l = len(datagram) + return struct.pack("!H", l) + datagram + + def _common_add_input(self, data, is_end): + self._buffer.put(data, is_end) + try: + return ( + self._expecting > 0 and self._buffer.have(self._expecting) + ) or self._buffer.seen_end + except UnexpectedEOF: + return True + + def _close(self): + self._connection.close_stream(self._stream_id) + self._buffer.put(b"", True) # send EOF in case we haven't seen it. + + +class BaseQuicConnection: + def __init__( + self, + connection, + address, + port, + source=None, + source_port=0, + manager=None, + ): + self._done = False + self._connection = connection + self._address = address + self._port = port + self._closed = False + self._manager = manager + self._streams = {} + if manager is not None and manager.is_h3(): + self._h3_conn = aioquic.h3.connection.H3Connection(connection, False) + else: + self._h3_conn = None + self._af = dns.inet.af_for_address(address) + self._peer = dns.inet.low_level_address_tuple((address, port)) + if source is None and source_port != 0: + if self._af == socket.AF_INET: + source = "0.0.0.0" + elif self._af == socket.AF_INET6: + source = "::" + else: + raise NotImplementedError + if source: + self._source = (source, source_port) + else: + self._source = None + + def is_h3(self): + return self._h3_conn is not None + + def close_stream(self, stream_id): + del self._streams[stream_id] + + def send_headers(self, stream_id, headers, is_end=False): + assert self._h3_conn is not None + self._h3_conn.send_headers(stream_id, headers, is_end) + + def send_data(self, stream_id, data, is_end=False): + assert self._h3_conn is not None + self._h3_conn.send_data(stream_id, data, is_end) + + def _get_timer_values(self, closed_is_special=True): + now = time.time() + expiration = self._connection.get_timer() + if expiration is None: + expiration = now + 3600 # arbitrary "big" value + interval = max(expiration - now, 0) + if self._closed and closed_is_special: + # lower sleep interval to avoid a race in the closing process + # which can lead to higher latency closing due to sleeping when + # we have events. + interval = min(interval, 0.05) + return (expiration, interval) + + def _handle_timer(self, expiration): + now = time.time() + if expiration <= now: + self._connection.handle_timer(now) + + +class AsyncQuicConnection(BaseQuicConnection): + async def make_stream(self, timeout: float | None = None) -> Any: + pass + + +class BaseQuicManager: + def __init__( + self, conf, verify_mode, connection_factory, server_name=None, h3=False + ): + self._connections = {} + self._connection_factory = connection_factory + self._session_tickets = {} + self._tokens = {} + self._h3 = h3 + if conf is None: + verify_path = None + if isinstance(verify_mode, str): + verify_path = verify_mode + verify_mode = True + if h3: + alpn_protocols = ["h3"] + else: + alpn_protocols = ["doq", "doq-i03"] + conf = aioquic.quic.configuration.QuicConfiguration( + alpn_protocols=alpn_protocols, + verify_mode=verify_mode, + server_name=server_name, + ) + if verify_path is not None: + cafile, capath = dns._tls_util.convert_verify_to_cafile_and_capath( + verify_path + ) + conf.load_verify_locations(cafile=cafile, capath=capath) + self._conf = conf + + def _connect( + self, + address, + port=853, + source=None, + source_port=0, + want_session_ticket=True, + want_token=True, + ): + connection = self._connections.get((address, port)) + if connection is not None: + return (connection, False) + conf = self._conf + if want_session_ticket: + try: + session_ticket = self._session_tickets.pop((address, port)) + # We found a session ticket, so make a configuration that uses it. + conf = copy.copy(conf) + conf.session_ticket = session_ticket + except KeyError: + # No session ticket. + pass + # Whether or not we found a session ticket, we want a handler to save + # one. + session_ticket_handler = functools.partial( + self.save_session_ticket, address, port + ) + else: + session_ticket_handler = None + if want_token: + try: + token = self._tokens.pop((address, port)) + # We found a token, so make a configuration that uses it. + conf = copy.copy(conf) + conf.token = token + except KeyError: + # No token + pass + # Whether or not we found a token, we want a handler to save # one. + token_handler = functools.partial(self.save_token, address, port) + else: + token_handler = None + + qconn = aioquic.quic.connection.QuicConnection( + configuration=conf, + session_ticket_handler=session_ticket_handler, + token_handler=token_handler, + ) + lladdress = dns.inet.low_level_address_tuple((address, port)) + qconn.connect(lladdress, time.time()) + connection = self._connection_factory( + qconn, address, port, source, source_port, self + ) + self._connections[(address, port)] = connection + return (connection, True) + + def closed(self, address, port): + try: + del self._connections[(address, port)] + except KeyError: + pass + + def is_h3(self): + return self._h3 + + def save_session_ticket(self, address, port, ticket): + # We rely on dictionaries keys() being in insertion order here. We + # can't just popitem() as that would be LIFO which is the opposite of + # what we want. + l = len(self._session_tickets) + if l >= MAX_SESSION_TICKETS: + keys_to_delete = list(self._session_tickets.keys())[0:SESSIONS_TO_DELETE] + for key in keys_to_delete: + del self._session_tickets[key] + self._session_tickets[(address, port)] = ticket + + def save_token(self, address, port, token): + # We rely on dictionaries keys() being in insertion order here. We + # can't just popitem() as that would be LIFO which is the opposite of + # what we want. + l = len(self._tokens) + if l >= MAX_SESSION_TICKETS: + keys_to_delete = list(self._tokens.keys())[0:SESSIONS_TO_DELETE] + for key in keys_to_delete: + del self._tokens[key] + self._tokens[(address, port)] = token + + +class AsyncQuicManager(BaseQuicManager): + def connect(self, address, port=853, source=None, source_port=0): + raise NotImplementedError diff --git a/netdeploy/lib/python3.11/site-packages/dns/quic/_sync.py b/netdeploy/lib/python3.11/site-packages/dns/quic/_sync.py new file mode 100644 index 0000000..18f9d05 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/quic/_sync.py @@ -0,0 +1,306 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import selectors +import socket +import ssl +import struct +import threading +import time + +import aioquic.h3.connection # type: ignore +import aioquic.h3.events # type: ignore +import aioquic.quic.configuration # type: ignore +import aioquic.quic.connection # type: ignore +import aioquic.quic.events # type: ignore + +import dns.exception +import dns.inet +from dns.quic._common import ( + QUIC_MAX_DATAGRAM, + BaseQuicConnection, + BaseQuicManager, + BaseQuicStream, + UnexpectedEOF, +) + +# Function used to create a socket. Can be overridden if needed in special +# situations. +socket_factory = socket.socket + + +class SyncQuicStream(BaseQuicStream): + def __init__(self, connection, stream_id): + super().__init__(connection, stream_id) + self._wake_up = threading.Condition() + self._lock = threading.Lock() + + def wait_for(self, amount, expiration): + while True: + timeout = self._timeout_from_expiration(expiration) + with self._lock: + if self._buffer.have(amount): + return + self._expecting = amount + with self._wake_up: + if not self._wake_up.wait(timeout): + raise dns.exception.Timeout + self._expecting = 0 + + def wait_for_end(self, expiration): + while True: + timeout = self._timeout_from_expiration(expiration) + with self._lock: + if self._buffer.seen_end(): + return + with self._wake_up: + if not self._wake_up.wait(timeout): + raise dns.exception.Timeout + + def receive(self, timeout=None): + expiration = self._expiration_from_timeout(timeout) + if self._connection.is_h3(): + self.wait_for_end(expiration) + with self._lock: + return self._buffer.get_all() + else: + self.wait_for(2, expiration) + with self._lock: + (size,) = struct.unpack("!H", self._buffer.get(2)) + self.wait_for(size, expiration) + with self._lock: + return self._buffer.get(size) + + def send(self, datagram, is_end=False): + data = self._encapsulate(datagram) + self._connection.write(self._stream_id, data, is_end) + + def _add_input(self, data, is_end): + if self._common_add_input(data, is_end): + with self._wake_up: + self._wake_up.notify() + + def close(self): + with self._lock: + self._close() + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.close() + with self._wake_up: + self._wake_up.notify() + return False + + +class SyncQuicConnection(BaseQuicConnection): + def __init__(self, connection, address, port, source, source_port, manager): + super().__init__(connection, address, port, source, source_port, manager) + self._socket = socket_factory(self._af, socket.SOCK_DGRAM, 0) + if self._source is not None: + try: + self._socket.bind( + dns.inet.low_level_address_tuple(self._source, self._af) + ) + except Exception: + self._socket.close() + raise + self._socket.connect(self._peer) + (self._send_wakeup, self._receive_wakeup) = socket.socketpair() + self._receive_wakeup.setblocking(False) + self._socket.setblocking(False) + self._handshake_complete = threading.Event() + self._worker_thread = None + self._lock = threading.Lock() + + def _read(self): + count = 0 + while count < 10: + count += 1 + try: + datagram = self._socket.recv(QUIC_MAX_DATAGRAM) + except BlockingIOError: + return + with self._lock: + self._connection.receive_datagram(datagram, self._peer, time.time()) + + def _drain_wakeup(self): + while True: + try: + self._receive_wakeup.recv(32) + except BlockingIOError: + return + + def _worker(self): + try: + with selectors.DefaultSelector() as sel: + sel.register(self._socket, selectors.EVENT_READ, self._read) + sel.register( + self._receive_wakeup, selectors.EVENT_READ, self._drain_wakeup + ) + while not self._done: + (expiration, interval) = self._get_timer_values(False) + items = sel.select(interval) + for key, _ in items: + key.data() + with self._lock: + self._handle_timer(expiration) + self._handle_events() + with self._lock: + datagrams = self._connection.datagrams_to_send(time.time()) + for datagram, _ in datagrams: + try: + self._socket.send(datagram) + except BlockingIOError: + # we let QUIC handle any lossage + pass + except Exception: + # Eat all exceptions as we have no way to pass them back to the + # caller currently. It might be nice to fix this in the future. + pass + finally: + with self._lock: + self._done = True + self._socket.close() + # Ensure anyone waiting for this gets woken up. + self._handshake_complete.set() + + def _handle_events(self): + while True: + with self._lock: + event = self._connection.next_event() + if event is None: + return + if isinstance(event, aioquic.quic.events.StreamDataReceived): + if self.is_h3(): + assert self._h3_conn is not None + h3_events = self._h3_conn.handle_event(event) + for h3_event in h3_events: + if isinstance(h3_event, aioquic.h3.events.HeadersReceived): + with self._lock: + stream = self._streams.get(event.stream_id) + if stream: + if stream._headers is None: + stream._headers = h3_event.headers + elif stream._trailers is None: + stream._trailers = h3_event.headers + if h3_event.stream_ended: + stream._add_input(b"", True) + elif isinstance(h3_event, aioquic.h3.events.DataReceived): + with self._lock: + stream = self._streams.get(event.stream_id) + if stream: + stream._add_input(h3_event.data, h3_event.stream_ended) + else: + with self._lock: + stream = self._streams.get(event.stream_id) + if stream: + stream._add_input(event.data, event.end_stream) + elif isinstance(event, aioquic.quic.events.HandshakeCompleted): + self._handshake_complete.set() + elif isinstance(event, aioquic.quic.events.ConnectionTerminated): + with self._lock: + self._done = True + elif isinstance(event, aioquic.quic.events.StreamReset): + with self._lock: + stream = self._streams.get(event.stream_id) + if stream: + stream._add_input(b"", True) + + def write(self, stream, data, is_end=False): + with self._lock: + self._connection.send_stream_data(stream, data, is_end) + self._send_wakeup.send(b"\x01") + + def send_headers(self, stream_id, headers, is_end=False): + with self._lock: + super().send_headers(stream_id, headers, is_end) + if is_end: + self._send_wakeup.send(b"\x01") + + def send_data(self, stream_id, data, is_end=False): + with self._lock: + super().send_data(stream_id, data, is_end) + if is_end: + self._send_wakeup.send(b"\x01") + + def run(self): + if self._closed: + return + self._worker_thread = threading.Thread(target=self._worker) + self._worker_thread.start() + + def make_stream(self, timeout=None): + if not self._handshake_complete.wait(timeout): + raise dns.exception.Timeout + with self._lock: + if self._done: + raise UnexpectedEOF + stream_id = self._connection.get_next_available_stream_id(False) + stream = SyncQuicStream(self, stream_id) + self._streams[stream_id] = stream + return stream + + def close_stream(self, stream_id): + with self._lock: + super().close_stream(stream_id) + + def close(self): + with self._lock: + if self._closed: + return + if self._manager is not None: + self._manager.closed(self._peer[0], self._peer[1]) + self._closed = True + self._connection.close() + self._send_wakeup.send(b"\x01") + if self._worker_thread is not None: + self._worker_thread.join() + + +class SyncQuicManager(BaseQuicManager): + def __init__( + self, conf=None, verify_mode=ssl.CERT_REQUIRED, server_name=None, h3=False + ): + super().__init__(conf, verify_mode, SyncQuicConnection, server_name, h3) + self._lock = threading.Lock() + + def connect( + self, + address, + port=853, + source=None, + source_port=0, + want_session_ticket=True, + want_token=True, + ): + with self._lock: + (connection, start) = self._connect( + address, port, source, source_port, want_session_ticket, want_token + ) + if start: + connection.run() + return connection + + def closed(self, address, port): + with self._lock: + super().closed(address, port) + + def save_session_ticket(self, address, port, ticket): + with self._lock: + super().save_session_ticket(address, port, ticket) + + def save_token(self, address, port, token): + with self._lock: + super().save_token(address, port, token) + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + # Copy the iterator into a list as exiting things will mutate the connections + # table. + connections = list(self._connections.values()) + for connection in connections: + connection.close() + return False diff --git a/netdeploy/lib/python3.11/site-packages/dns/quic/_trio.py b/netdeploy/lib/python3.11/site-packages/dns/quic/_trio.py new file mode 100644 index 0000000..046e6aa --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/quic/_trio.py @@ -0,0 +1,250 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import socket +import ssl +import struct +import time + +import aioquic.h3.connection # type: ignore +import aioquic.h3.events # type: ignore +import aioquic.quic.configuration # type: ignore +import aioquic.quic.connection # type: ignore +import aioquic.quic.events # type: ignore +import trio + +import dns.exception +import dns.inet +from dns._asyncbackend import NullContext +from dns.quic._common import ( + QUIC_MAX_DATAGRAM, + AsyncQuicConnection, + AsyncQuicManager, + BaseQuicStream, + UnexpectedEOF, +) + + +class TrioQuicStream(BaseQuicStream): + def __init__(self, connection, stream_id): + super().__init__(connection, stream_id) + self._wake_up = trio.Condition() + + async def wait_for(self, amount): + while True: + if self._buffer.have(amount): + return + self._expecting = amount + async with self._wake_up: + await self._wake_up.wait() + self._expecting = 0 + + async def wait_for_end(self): + while True: + if self._buffer.seen_end(): + return + async with self._wake_up: + await self._wake_up.wait() + + async def receive(self, timeout=None): + if timeout is None: + context = NullContext(None) + else: + context = trio.move_on_after(timeout) + with context: + if self._connection.is_h3(): + await self.wait_for_end() + return self._buffer.get_all() + else: + await self.wait_for(2) + (size,) = struct.unpack("!H", self._buffer.get(2)) + await self.wait_for(size) + return self._buffer.get(size) + raise dns.exception.Timeout + + async def send(self, datagram, is_end=False): + data = self._encapsulate(datagram) + await self._connection.write(self._stream_id, data, is_end) + + async def _add_input(self, data, is_end): + if self._common_add_input(data, is_end): + async with self._wake_up: + self._wake_up.notify() + + async def close(self): + self._close() + + # Streams are async context managers + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.close() + async with self._wake_up: + self._wake_up.notify() + return False + + +class TrioQuicConnection(AsyncQuicConnection): + def __init__(self, connection, address, port, source, source_port, manager=None): + super().__init__(connection, address, port, source, source_port, manager) + self._socket = trio.socket.socket(self._af, socket.SOCK_DGRAM, 0) + self._handshake_complete = trio.Event() + self._run_done = trio.Event() + self._worker_scope = None + self._send_pending = False + + async def _worker(self): + try: + if self._source: + await self._socket.bind( + dns.inet.low_level_address_tuple(self._source, self._af) + ) + await self._socket.connect(self._peer) + while not self._done: + (expiration, interval) = self._get_timer_values(False) + if self._send_pending: + # Do not block forever if sends are pending. Even though we + # have a wake-up mechanism if we've already started the blocking + # read, the possibility of context switching in send means that + # more writes can happen while we have no wake up context, so + # we need self._send_pending to avoid (effectively) a "lost wakeup" + # race. + interval = 0.0 + with trio.CancelScope( + deadline=trio.current_time() + interval # pyright: ignore + ) as self._worker_scope: + datagram = await self._socket.recv(QUIC_MAX_DATAGRAM) + self._connection.receive_datagram(datagram, self._peer, time.time()) + self._worker_scope = None + self._handle_timer(expiration) + await self._handle_events() + # We clear this now, before sending anything, as sending can cause + # context switches that do more sends. We want to know if that + # happens so we don't block a long time on the recv() above. + self._send_pending = False + datagrams = self._connection.datagrams_to_send(time.time()) + for datagram, _ in datagrams: + await self._socket.send(datagram) + finally: + self._done = True + self._socket.close() + self._handshake_complete.set() + + async def _handle_events(self): + count = 0 + while True: + event = self._connection.next_event() + if event is None: + return + if isinstance(event, aioquic.quic.events.StreamDataReceived): + if self.is_h3(): + assert self._h3_conn is not None + h3_events = self._h3_conn.handle_event(event) + for h3_event in h3_events: + if isinstance(h3_event, aioquic.h3.events.HeadersReceived): + stream = self._streams.get(event.stream_id) + if stream: + if stream._headers is None: + stream._headers = h3_event.headers + elif stream._trailers is None: + stream._trailers = h3_event.headers + if h3_event.stream_ended: + await stream._add_input(b"", True) + elif isinstance(h3_event, aioquic.h3.events.DataReceived): + stream = self._streams.get(event.stream_id) + if stream: + await stream._add_input( + h3_event.data, h3_event.stream_ended + ) + else: + stream = self._streams.get(event.stream_id) + if stream: + await stream._add_input(event.data, event.end_stream) + elif isinstance(event, aioquic.quic.events.HandshakeCompleted): + self._handshake_complete.set() + elif isinstance(event, aioquic.quic.events.ConnectionTerminated): + self._done = True + self._socket.close() + elif isinstance(event, aioquic.quic.events.StreamReset): + stream = self._streams.get(event.stream_id) + if stream: + await stream._add_input(b"", True) + count += 1 + if count > 10: + # yield + count = 0 + await trio.sleep(0) + + async def write(self, stream, data, is_end=False): + self._connection.send_stream_data(stream, data, is_end) + self._send_pending = True + if self._worker_scope is not None: + self._worker_scope.cancel() + + async def run(self): + if self._closed: + return + async with trio.open_nursery() as nursery: + nursery.start_soon(self._worker) + self._run_done.set() + + async def make_stream(self, timeout=None): + if timeout is None: + context = NullContext(None) + else: + context = trio.move_on_after(timeout) + with context: + await self._handshake_complete.wait() + if self._done: + raise UnexpectedEOF + stream_id = self._connection.get_next_available_stream_id(False) + stream = TrioQuicStream(self, stream_id) + self._streams[stream_id] = stream + return stream + raise dns.exception.Timeout + + async def close(self): + if not self._closed: + if self._manager is not None: + self._manager.closed(self._peer[0], self._peer[1]) + self._closed = True + self._connection.close() + self._send_pending = True + if self._worker_scope is not None: + self._worker_scope.cancel() + await self._run_done.wait() + + +class TrioQuicManager(AsyncQuicManager): + def __init__( + self, + nursery, + conf=None, + verify_mode=ssl.CERT_REQUIRED, + server_name=None, + h3=False, + ): + super().__init__(conf, verify_mode, TrioQuicConnection, server_name, h3) + self._nursery = nursery + + def connect( + self, address, port=853, source=None, source_port=0, want_session_ticket=True + ): + (connection, start) = self._connect( + address, port, source, source_port, want_session_ticket + ) + if start: + self._nursery.start_soon(connection.run) + return connection + + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + # Copy the iterator into a list as exiting things will mutate the connections + # table. + connections = list(self._connections.values()) + for connection in connections: + await connection.close() + return False diff --git a/netdeploy/lib/python3.11/site-packages/dns/rcode.py b/netdeploy/lib/python3.11/site-packages/dns/rcode.py new file mode 100644 index 0000000..7bb8467 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rcode.py @@ -0,0 +1,168 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Result Codes.""" + +from typing import Tuple, Type + +import dns.enum +import dns.exception + + +class Rcode(dns.enum.IntEnum): + #: No error + NOERROR = 0 + #: Format error + FORMERR = 1 + #: Server failure + SERVFAIL = 2 + #: Name does not exist ("Name Error" in RFC 1025 terminology). + NXDOMAIN = 3 + #: Not implemented + NOTIMP = 4 + #: Refused + REFUSED = 5 + #: Name exists. + YXDOMAIN = 6 + #: RRset exists. + YXRRSET = 7 + #: RRset does not exist. + NXRRSET = 8 + #: Not authoritative. + NOTAUTH = 9 + #: Name not in zone. + NOTZONE = 10 + #: DSO-TYPE Not Implemented + DSOTYPENI = 11 + #: Bad EDNS version. + BADVERS = 16 + #: TSIG Signature Failure + BADSIG = 16 + #: Key not recognized. + BADKEY = 17 + #: Signature out of time window. + BADTIME = 18 + #: Bad TKEY Mode. + BADMODE = 19 + #: Duplicate key name. + BADNAME = 20 + #: Algorithm not supported. + BADALG = 21 + #: Bad Truncation + BADTRUNC = 22 + #: Bad/missing Server Cookie + BADCOOKIE = 23 + + @classmethod + def _maximum(cls): + return 4095 + + @classmethod + def _unknown_exception_class(cls) -> Type[Exception]: + return UnknownRcode + + +class UnknownRcode(dns.exception.DNSException): + """A DNS rcode is unknown.""" + + +def from_text(text: str) -> Rcode: + """Convert text into an rcode. + + *text*, a ``str``, the textual rcode or an integer in textual form. + + Raises ``dns.rcode.UnknownRcode`` if the rcode mnemonic is unknown. + + Returns a ``dns.rcode.Rcode``. + """ + + return Rcode.from_text(text) + + +def from_flags(flags: int, ednsflags: int) -> Rcode: + """Return the rcode value encoded by flags and ednsflags. + + *flags*, an ``int``, the DNS flags field. + + *ednsflags*, an ``int``, the EDNS flags field. + + Raises ``ValueError`` if rcode is < 0 or > 4095 + + Returns a ``dns.rcode.Rcode``. + """ + + value = (flags & 0x000F) | ((ednsflags >> 20) & 0xFF0) + return Rcode.make(value) + + +def to_flags(value: Rcode) -> Tuple[int, int]: + """Return a (flags, ednsflags) tuple which encodes the rcode. + + *value*, a ``dns.rcode.Rcode``, the rcode. + + Raises ``ValueError`` if rcode is < 0 or > 4095. + + Returns an ``(int, int)`` tuple. + """ + + if value < 0 or value > 4095: + raise ValueError("rcode must be >= 0 and <= 4095") + v = value & 0xF + ev = (value & 0xFF0) << 20 + return (v, ev) + + +def to_text(value: Rcode, tsig: bool = False) -> str: + """Convert rcode into text. + + *value*, a ``dns.rcode.Rcode``, the rcode. + + Raises ``ValueError`` if rcode is < 0 or > 4095. + + Returns a ``str``. + """ + + if tsig and value == Rcode.BADVERS: + return "BADSIG" + return Rcode.to_text(value) + + +### BEGIN generated Rcode constants + +NOERROR = Rcode.NOERROR +FORMERR = Rcode.FORMERR +SERVFAIL = Rcode.SERVFAIL +NXDOMAIN = Rcode.NXDOMAIN +NOTIMP = Rcode.NOTIMP +REFUSED = Rcode.REFUSED +YXDOMAIN = Rcode.YXDOMAIN +YXRRSET = Rcode.YXRRSET +NXRRSET = Rcode.NXRRSET +NOTAUTH = Rcode.NOTAUTH +NOTZONE = Rcode.NOTZONE +DSOTYPENI = Rcode.DSOTYPENI +BADVERS = Rcode.BADVERS +BADSIG = Rcode.BADSIG +BADKEY = Rcode.BADKEY +BADTIME = Rcode.BADTIME +BADMODE = Rcode.BADMODE +BADNAME = Rcode.BADNAME +BADALG = Rcode.BADALG +BADTRUNC = Rcode.BADTRUNC +BADCOOKIE = Rcode.BADCOOKIE + +### END generated Rcode constants diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdata.py b/netdeploy/lib/python3.11/site-packages/dns/rdata.py new file mode 100644 index 0000000..c4522e6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdata.py @@ -0,0 +1,935 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS rdata.""" + +import base64 +import binascii +import inspect +import io +import ipaddress +import itertools +import random +from importlib import import_module +from typing import Any, Dict, Tuple + +import dns.exception +import dns.immutable +import dns.ipv4 +import dns.ipv6 +import dns.name +import dns.rdataclass +import dns.rdatatype +import dns.tokenizer +import dns.ttl +import dns.wire + +_chunksize = 32 + +# We currently allow comparisons for rdata with relative names for backwards +# compatibility, but in the future we will not, as these kinds of comparisons +# can lead to subtle bugs if code is not carefully written. +# +# This switch allows the future behavior to be turned on so code can be +# tested with it. +_allow_relative_comparisons = True + + +class NoRelativeRdataOrdering(dns.exception.DNSException): + """An attempt was made to do an ordered comparison of one or more + rdata with relative names. The only reliable way of sorting rdata + is to use non-relativized rdata. + + """ + + +def _wordbreak(data, chunksize=_chunksize, separator=b" "): + """Break a binary string into chunks of chunksize characters separated by + a space. + """ + + if not chunksize: + return data.decode() + return separator.join( + [data[i : i + chunksize] for i in range(0, len(data), chunksize)] + ).decode() + + +# pylint: disable=unused-argument + + +def _hexify(data, chunksize=_chunksize, separator=b" ", **kw): + """Convert a binary string into its hex encoding, broken up into chunks + of chunksize characters separated by a separator. + """ + + return _wordbreak(binascii.hexlify(data), chunksize, separator) + + +def _base64ify(data, chunksize=_chunksize, separator=b" ", **kw): + """Convert a binary string into its base64 encoding, broken up into chunks + of chunksize characters separated by a separator. + """ + + return _wordbreak(base64.b64encode(data), chunksize, separator) + + +# pylint: enable=unused-argument + +__escaped = b'"\\' + + +def _escapify(qstring): + """Escape the characters in a quoted string which need it.""" + + if isinstance(qstring, str): + qstring = qstring.encode() + if not isinstance(qstring, bytearray): + qstring = bytearray(qstring) + + text = "" + for c in qstring: + if c in __escaped: + text += "\\" + chr(c) + elif c >= 0x20 and c < 0x7F: + text += chr(c) + else: + text += f"\\{c:03d}" + return text + + +def _truncate_bitmap(what): + """Determine the index of greatest byte that isn't all zeros, and + return the bitmap that contains all the bytes less than that index. + """ + + for i in range(len(what) - 1, -1, -1): + if what[i] != 0: + return what[0 : i + 1] + return what[0:1] + + +# So we don't have to edit all the rdata classes... +_constify = dns.immutable.constify + + +@dns.immutable.immutable +class Rdata: + """Base class for all DNS rdata types.""" + + __slots__ = ["rdclass", "rdtype", "rdcomment"] + + def __init__( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + ) -> None: + """Initialize an rdata. + + *rdclass*, an ``int`` is the rdataclass of the Rdata. + + *rdtype*, an ``int`` is the rdatatype of the Rdata. + """ + + self.rdclass = self._as_rdataclass(rdclass) + self.rdtype = self._as_rdatatype(rdtype) + self.rdcomment = None + + def _get_all_slots(self): + return itertools.chain.from_iterable( + getattr(cls, "__slots__", []) for cls in self.__class__.__mro__ + ) + + def __getstate__(self): + # We used to try to do a tuple of all slots here, but it + # doesn't work as self._all_slots isn't available at + # __setstate__() time. Before that we tried to store a tuple + # of __slots__, but that didn't work as it didn't store the + # slots defined by ancestors. This older way didn't fail + # outright, but ended up with partially broken objects, e.g. + # if you unpickled an A RR it wouldn't have rdclass and rdtype + # attributes, and would compare badly. + state = {} + for slot in self._get_all_slots(): + state[slot] = getattr(self, slot) + return state + + def __setstate__(self, state): + for slot, val in state.items(): + object.__setattr__(self, slot, val) + if not hasattr(self, "rdcomment"): + # Pickled rdata from 2.0.x might not have a rdcomment, so add + # it if needed. + object.__setattr__(self, "rdcomment", None) + + def covers(self) -> dns.rdatatype.RdataType: + """Return the type a Rdata covers. + + DNS SIG/RRSIG rdatas apply to a specific type; this type is + returned by the covers() function. If the rdata type is not + SIG or RRSIG, dns.rdatatype.NONE is returned. This is useful when + creating rdatasets, allowing the rdataset to contain only RRSIGs + of a particular type, e.g. RRSIG(NS). + + Returns a ``dns.rdatatype.RdataType``. + """ + + return dns.rdatatype.NONE + + def extended_rdatatype(self) -> int: + """Return a 32-bit type value, the least significant 16 bits of + which are the ordinary DNS type, and the upper 16 bits of which are + the "covered" type, if any. + + Returns an ``int``. + """ + + return self.covers() << 16 | self.rdtype + + def to_text( + self, + origin: dns.name.Name | None = None, + relativize: bool = True, + **kw: Dict[str, Any], + ) -> str: + """Convert an rdata to text format. + + Returns a ``str``. + """ + + raise NotImplementedError # pragma: no cover + + def _to_wire( + self, + file: Any, + compress: dns.name.CompressType | None = None, + origin: dns.name.Name | None = None, + canonicalize: bool = False, + ) -> None: + raise NotImplementedError # pragma: no cover + + def to_wire( + self, + file: Any | None = None, + compress: dns.name.CompressType | None = None, + origin: dns.name.Name | None = None, + canonicalize: bool = False, + ) -> bytes | None: + """Convert an rdata to wire format. + + Returns a ``bytes`` if no output file was specified, or ``None`` otherwise. + """ + + if file: + # We call _to_wire() and then return None explicitly instead of + # of just returning the None from _to_wire() as mypy's func-returns-value + # unhelpfully errors out with "error: "_to_wire" of "Rdata" does not return + # a value (it only ever returns None)" + self._to_wire(file, compress, origin, canonicalize) + return None + else: + f = io.BytesIO() + self._to_wire(f, compress, origin, canonicalize) + return f.getvalue() + + def to_generic(self, origin: dns.name.Name | None = None) -> "GenericRdata": + """Creates a dns.rdata.GenericRdata equivalent of this rdata. + + Returns a ``dns.rdata.GenericRdata``. + """ + wire = self.to_wire(origin=origin) + assert wire is not None # for type checkers + return GenericRdata(self.rdclass, self.rdtype, wire) + + def to_digestable(self, origin: dns.name.Name | None = None) -> bytes: + """Convert rdata to a format suitable for digesting in hashes. This + is also the DNSSEC canonical form. + + Returns a ``bytes``. + """ + wire = self.to_wire(origin=origin, canonicalize=True) + assert wire is not None # for mypy + return wire + + def __repr__(self): + covers = self.covers() + if covers == dns.rdatatype.NONE: + ctext = "" + else: + ctext = "(" + dns.rdatatype.to_text(covers) + ")" + return ( + "" + ) + + def __str__(self): + return self.to_text() + + def _cmp(self, other): + """Compare an rdata with another rdata of the same rdtype and + rdclass. + + For rdata with only absolute names: + Return < 0 if self < other in the DNSSEC ordering, 0 if self + == other, and > 0 if self > other. + For rdata with at least one relative names: + The rdata sorts before any rdata with only absolute names. + When compared with another relative rdata, all names are + made absolute as if they were relative to the root, as the + proper origin is not available. While this creates a stable + ordering, it is NOT guaranteed to be the DNSSEC ordering. + In the future, all ordering comparisons for rdata with + relative names will be disallowed. + """ + # the next two lines are for type checkers, so they are bound + our = b"" + their = b"" + try: + our = self.to_digestable() + our_relative = False + except dns.name.NeedAbsoluteNameOrOrigin: + if _allow_relative_comparisons: + our = self.to_digestable(dns.name.root) + our_relative = True + try: + their = other.to_digestable() + their_relative = False + except dns.name.NeedAbsoluteNameOrOrigin: + if _allow_relative_comparisons: + their = other.to_digestable(dns.name.root) + their_relative = True + if _allow_relative_comparisons: + if our_relative != their_relative: + # For the purpose of comparison, all rdata with at least one + # relative name is less than an rdata with only absolute names. + if our_relative: + return -1 + else: + return 1 + elif our_relative or their_relative: + raise NoRelativeRdataOrdering + if our == their: + return 0 + elif our > their: + return 1 + else: + return -1 + + def __eq__(self, other): + if not isinstance(other, Rdata): + return False + if self.rdclass != other.rdclass or self.rdtype != other.rdtype: + return False + our_relative = False + their_relative = False + try: + our = self.to_digestable() + except dns.name.NeedAbsoluteNameOrOrigin: + our = self.to_digestable(dns.name.root) + our_relative = True + try: + their = other.to_digestable() + except dns.name.NeedAbsoluteNameOrOrigin: + their = other.to_digestable(dns.name.root) + their_relative = True + if our_relative != their_relative: + return False + return our == their + + def __ne__(self, other): + if not isinstance(other, Rdata): + return True + if self.rdclass != other.rdclass or self.rdtype != other.rdtype: + return True + return not self.__eq__(other) + + def __lt__(self, other): + if ( + not isinstance(other, Rdata) + or self.rdclass != other.rdclass + or self.rdtype != other.rdtype + ): + return NotImplemented + return self._cmp(other) < 0 + + def __le__(self, other): + if ( + not isinstance(other, Rdata) + or self.rdclass != other.rdclass + or self.rdtype != other.rdtype + ): + return NotImplemented + return self._cmp(other) <= 0 + + def __ge__(self, other): + if ( + not isinstance(other, Rdata) + or self.rdclass != other.rdclass + or self.rdtype != other.rdtype + ): + return NotImplemented + return self._cmp(other) >= 0 + + def __gt__(self, other): + if ( + not isinstance(other, Rdata) + or self.rdclass != other.rdclass + or self.rdtype != other.rdtype + ): + return NotImplemented + return self._cmp(other) > 0 + + def __hash__(self): + return hash(self.to_digestable(dns.name.root)) + + @classmethod + def from_text( + cls, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + tok: dns.tokenizer.Tokenizer, + origin: dns.name.Name | None = None, + relativize: bool = True, + relativize_to: dns.name.Name | None = None, + ) -> "Rdata": + raise NotImplementedError # pragma: no cover + + @classmethod + def from_wire_parser( + cls, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + parser: dns.wire.Parser, + origin: dns.name.Name | None = None, + ) -> "Rdata": + raise NotImplementedError # pragma: no cover + + def replace(self, **kwargs: Any) -> "Rdata": + """ + Create a new Rdata instance based on the instance replace was + invoked on. It is possible to pass different parameters to + override the corresponding properties of the base Rdata. + + Any field specific to the Rdata type can be replaced, but the + *rdtype* and *rdclass* fields cannot. + + Returns an instance of the same Rdata subclass as *self*. + """ + + # Get the constructor parameters. + parameters = inspect.signature(self.__init__).parameters # type: ignore + + # Ensure that all of the arguments correspond to valid fields. + # Don't allow rdclass or rdtype to be changed, though. + for key in kwargs: + if key == "rdcomment": + continue + if key not in parameters: + raise AttributeError( + f"'{self.__class__.__name__}' object has no attribute '{key}'" + ) + if key in ("rdclass", "rdtype"): + raise AttributeError( + f"Cannot overwrite '{self.__class__.__name__}' attribute '{key}'" + ) + + # Construct the parameter list. For each field, use the value in + # kwargs if present, and the current value otherwise. + args = (kwargs.get(key, getattr(self, key)) for key in parameters) + + # Create, validate, and return the new object. + rd = self.__class__(*args) + # The comment is not set in the constructor, so give it special + # handling. + rdcomment = kwargs.get("rdcomment", self.rdcomment) + if rdcomment is not None: + object.__setattr__(rd, "rdcomment", rdcomment) + return rd + + # Type checking and conversion helpers. These are class methods as + # they don't touch object state and may be useful to others. + + @classmethod + def _as_rdataclass(cls, value): + return dns.rdataclass.RdataClass.make(value) + + @classmethod + def _as_rdatatype(cls, value): + return dns.rdatatype.RdataType.make(value) + + @classmethod + def _as_bytes( + cls, + value: Any, + encode: bool = False, + max_length: int | None = None, + empty_ok: bool = True, + ) -> bytes: + if encode and isinstance(value, str): + bvalue = value.encode() + elif isinstance(value, bytearray): + bvalue = bytes(value) + elif isinstance(value, bytes): + bvalue = value + else: + raise ValueError("not bytes") + if max_length is not None and len(bvalue) > max_length: + raise ValueError("too long") + if not empty_ok and len(bvalue) == 0: + raise ValueError("empty bytes not allowed") + return bvalue + + @classmethod + def _as_name(cls, value): + # Note that proper name conversion (e.g. with origin and IDNA + # awareness) is expected to be done via from_text. This is just + # a simple thing for people invoking the constructor directly. + if isinstance(value, str): + return dns.name.from_text(value) + elif not isinstance(value, dns.name.Name): + raise ValueError("not a name") + return value + + @classmethod + def _as_uint8(cls, value): + if not isinstance(value, int): + raise ValueError("not an integer") + if value < 0 or value > 255: + raise ValueError("not a uint8") + return value + + @classmethod + def _as_uint16(cls, value): + if not isinstance(value, int): + raise ValueError("not an integer") + if value < 0 or value > 65535: + raise ValueError("not a uint16") + return value + + @classmethod + def _as_uint32(cls, value): + if not isinstance(value, int): + raise ValueError("not an integer") + if value < 0 or value > 4294967295: + raise ValueError("not a uint32") + return value + + @classmethod + def _as_uint48(cls, value): + if not isinstance(value, int): + raise ValueError("not an integer") + if value < 0 or value > 281474976710655: + raise ValueError("not a uint48") + return value + + @classmethod + def _as_int(cls, value, low=None, high=None): + if not isinstance(value, int): + raise ValueError("not an integer") + if low is not None and value < low: + raise ValueError("value too small") + if high is not None and value > high: + raise ValueError("value too large") + return value + + @classmethod + def _as_ipv4_address(cls, value): + if isinstance(value, str): + return dns.ipv4.canonicalize(value) + elif isinstance(value, bytes): + return dns.ipv4.inet_ntoa(value) + elif isinstance(value, ipaddress.IPv4Address): + return dns.ipv4.inet_ntoa(value.packed) + else: + raise ValueError("not an IPv4 address") + + @classmethod + def _as_ipv6_address(cls, value): + if isinstance(value, str): + return dns.ipv6.canonicalize(value) + elif isinstance(value, bytes): + return dns.ipv6.inet_ntoa(value) + elif isinstance(value, ipaddress.IPv6Address): + return dns.ipv6.inet_ntoa(value.packed) + else: + raise ValueError("not an IPv6 address") + + @classmethod + def _as_bool(cls, value): + if isinstance(value, bool): + return value + else: + raise ValueError("not a boolean") + + @classmethod + def _as_ttl(cls, value): + if isinstance(value, int): + return cls._as_int(value, 0, dns.ttl.MAX_TTL) + elif isinstance(value, str): + return dns.ttl.from_text(value) + else: + raise ValueError("not a TTL") + + @classmethod + def _as_tuple(cls, value, as_value): + try: + # For user convenience, if value is a singleton of the list + # element type, wrap it in a tuple. + return (as_value(value),) + except Exception: + # Otherwise, check each element of the iterable *value* + # against *as_value*. + return tuple(as_value(v) for v in value) + + # Processing order + + @classmethod + def _processing_order(cls, iterable): + items = list(iterable) + random.shuffle(items) + return items + + +@dns.immutable.immutable +class GenericRdata(Rdata): + """Generic Rdata Class + + This class is used for rdata types for which we have no better + implementation. It implements the DNS "unknown RRs" scheme. + """ + + __slots__ = ["data"] + + def __init__( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + data: bytes, + ) -> None: + super().__init__(rdclass, rdtype) + self.data = data + + def to_text( + self, + origin: dns.name.Name | None = None, + relativize: bool = True, + **kw: Dict[str, Any], + ) -> str: + return rf"\# {len(self.data)} " + _hexify(self.data, **kw) # pyright: ignore + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + token = tok.get() + if not token.is_identifier() or token.value != r"\#": + raise dns.exception.SyntaxError(r"generic rdata does not start with \#") + length = tok.get_int() + hex = tok.concatenate_remaining_identifiers(True).encode() + data = binascii.unhexlify(hex) + if len(data) != length: + raise dns.exception.SyntaxError("generic rdata hex data has wrong length") + return cls(rdclass, rdtype, data) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(self.data) + + def to_generic(self, origin: dns.name.Name | None = None) -> "GenericRdata": + return self + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + return cls(rdclass, rdtype, parser.get_remaining()) + + +_rdata_classes: Dict[Tuple[dns.rdataclass.RdataClass, dns.rdatatype.RdataType], Any] = ( + {} +) +_module_prefix = "dns.rdtypes" +_dynamic_load_allowed = True + + +def get_rdata_class(rdclass, rdtype, use_generic=True): + cls = _rdata_classes.get((rdclass, rdtype)) + if not cls: + cls = _rdata_classes.get((dns.rdataclass.ANY, rdtype)) + if not cls and _dynamic_load_allowed: + rdclass_text = dns.rdataclass.to_text(rdclass) + rdtype_text = dns.rdatatype.to_text(rdtype) + rdtype_text = rdtype_text.replace("-", "_") + try: + mod = import_module( + ".".join([_module_prefix, rdclass_text, rdtype_text]) + ) + cls = getattr(mod, rdtype_text) + _rdata_classes[(rdclass, rdtype)] = cls + except ImportError: + try: + mod = import_module(".".join([_module_prefix, "ANY", rdtype_text])) + cls = getattr(mod, rdtype_text) + _rdata_classes[(dns.rdataclass.ANY, rdtype)] = cls + _rdata_classes[(rdclass, rdtype)] = cls + except ImportError: + pass + if not cls and use_generic: + cls = GenericRdata + _rdata_classes[(rdclass, rdtype)] = cls + return cls + + +def load_all_types(disable_dynamic_load=True): + """Load all rdata types for which dnspython has a non-generic implementation. + + Normally dnspython loads DNS rdatatype implementations on demand, but in some + specialized cases loading all types at an application-controlled time is preferred. + + If *disable_dynamic_load*, a ``bool``, is ``True`` then dnspython will not attempt + to use its dynamic loading mechanism if an unknown type is subsequently encountered, + and will simply use the ``GenericRdata`` class. + """ + # Load class IN and ANY types. + for rdtype in dns.rdatatype.RdataType: + get_rdata_class(dns.rdataclass.IN, rdtype, False) + # Load the one non-ANY implementation we have in CH. Everything + # else in CH is an ANY type, and we'll discover those on demand but won't + # have to import anything. + get_rdata_class(dns.rdataclass.CH, dns.rdatatype.A, False) + if disable_dynamic_load: + # Now disable dynamic loading so any subsequent unknown type immediately becomes + # GenericRdata without a load attempt. + global _dynamic_load_allowed + _dynamic_load_allowed = False + + +def from_text( + rdclass: dns.rdataclass.RdataClass | str, + rdtype: dns.rdatatype.RdataType | str, + tok: dns.tokenizer.Tokenizer | str, + origin: dns.name.Name | None = None, + relativize: bool = True, + relativize_to: dns.name.Name | None = None, + idna_codec: dns.name.IDNACodec | None = None, +) -> Rdata: + """Build an rdata object from text format. + + This function attempts to dynamically load a class which + implements the specified rdata class and type. If there is no + class-and-type-specific implementation, the GenericRdata class + is used. + + Once a class is chosen, its from_text() class method is called + with the parameters to this function. + + If *tok* is a ``str``, then a tokenizer is created and the string + is used as its input. + + *rdclass*, a ``dns.rdataclass.RdataClass`` or ``str``, the rdataclass. + + *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdatatype. + + *tok*, a ``dns.tokenizer.Tokenizer`` or a ``str``. + + *origin*, a ``dns.name.Name`` (or ``None``), the + origin to use for relative names. + + *relativize*, a ``bool``. If true, name will be relativized. + + *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use + when relativizing names. If not set, the *origin* value will be used. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder to use if a tokenizer needs to be created. If + ``None``, the default IDNA 2003 encoder/decoder is used. If a + tokenizer is not created, then the codec associated with the tokenizer + is the one that is used. + + Returns an instance of the chosen Rdata subclass. + + """ + if isinstance(tok, str): + tok = dns.tokenizer.Tokenizer(tok, idna_codec=idna_codec) + if not isinstance(tok, dns.tokenizer.Tokenizer): + raise ValueError("tok must be a string or a Tokenizer") + rdclass = dns.rdataclass.RdataClass.make(rdclass) + rdtype = dns.rdatatype.RdataType.make(rdtype) + cls = get_rdata_class(rdclass, rdtype) + assert cls is not None # for type checkers + with dns.exception.ExceptionWrapper(dns.exception.SyntaxError): + rdata = None + if cls != GenericRdata: + # peek at first token + token = tok.get() + tok.unget(token) + if token.is_identifier() and token.value == r"\#": + # + # Known type using the generic syntax. Extract the + # wire form from the generic syntax, and then run + # from_wire on it. + # + grdata = GenericRdata.from_text( + rdclass, rdtype, tok, origin, relativize, relativize_to + ) + rdata = from_wire( + rdclass, rdtype, grdata.data, 0, len(grdata.data), origin + ) + # + # If this comparison isn't equal, then there must have been + # compressed names in the wire format, which is an error, + # there being no reasonable context to decompress with. + # + rwire = rdata.to_wire() + if rwire != grdata.data: + raise dns.exception.SyntaxError( + "compressed data in " + "generic syntax form " + "of known rdatatype" + ) + if rdata is None: + rdata = cls.from_text( + rdclass, rdtype, tok, origin, relativize, relativize_to + ) + token = tok.get_eol_as_token() + if token.comment is not None: + object.__setattr__(rdata, "rdcomment", token.comment) + return rdata + + +def from_wire_parser( + rdclass: dns.rdataclass.RdataClass | str, + rdtype: dns.rdatatype.RdataType | str, + parser: dns.wire.Parser, + origin: dns.name.Name | None = None, +) -> Rdata: + """Build an rdata object from wire format + + This function attempts to dynamically load a class which + implements the specified rdata class and type. If there is no + class-and-type-specific implementation, the GenericRdata class + is used. + + Once a class is chosen, its from_wire() class method is called + with the parameters to this function. + + *rdclass*, a ``dns.rdataclass.RdataClass`` or ``str``, the rdataclass. + + *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdatatype. + + *parser*, a ``dns.wire.Parser``, the parser, which should be + restricted to the rdata length. + + *origin*, a ``dns.name.Name`` (or ``None``). If not ``None``, + then names will be relativized to this origin. + + Returns an instance of the chosen Rdata subclass. + """ + + rdclass = dns.rdataclass.RdataClass.make(rdclass) + rdtype = dns.rdatatype.RdataType.make(rdtype) + cls = get_rdata_class(rdclass, rdtype) + assert cls is not None # for type checkers + with dns.exception.ExceptionWrapper(dns.exception.FormError): + return cls.from_wire_parser(rdclass, rdtype, parser, origin) + + +def from_wire( + rdclass: dns.rdataclass.RdataClass | str, + rdtype: dns.rdatatype.RdataType | str, + wire: bytes, + current: int, + rdlen: int, + origin: dns.name.Name | None = None, +) -> Rdata: + """Build an rdata object from wire format + + This function attempts to dynamically load a class which + implements the specified rdata class and type. If there is no + class-and-type-specific implementation, the GenericRdata class + is used. + + Once a class is chosen, its from_wire() class method is called + with the parameters to this function. + + *rdclass*, an ``int``, the rdataclass. + + *rdtype*, an ``int``, the rdatatype. + + *wire*, a ``bytes``, the wire-format message. + + *current*, an ``int``, the offset in wire of the beginning of + the rdata. + + *rdlen*, an ``int``, the length of the wire-format rdata + + *origin*, a ``dns.name.Name`` (or ``None``). If not ``None``, + then names will be relativized to this origin. + + Returns an instance of the chosen Rdata subclass. + """ + parser = dns.wire.Parser(wire, current) + with parser.restrict_to(rdlen): + return from_wire_parser(rdclass, rdtype, parser, origin) + + +class RdatatypeExists(dns.exception.DNSException): + """DNS rdatatype already exists.""" + + supp_kwargs = {"rdclass", "rdtype"} + fmt = ( + "The rdata type with class {rdclass:d} and rdtype {rdtype:d} " + + "already exists." + ) + + +def register_type( + implementation: Any, + rdtype: int, + rdtype_text: str, + is_singleton: bool = False, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, +) -> None: + """Dynamically register a module to handle an rdatatype. + + *implementation*, a subclass of ``dns.rdata.Rdata`` implementing the type, + or a module containing such a class named by its text form. + + *rdtype*, an ``int``, the rdatatype to register. + + *rdtype_text*, a ``str``, the textual form of the rdatatype. + + *is_singleton*, a ``bool``, indicating if the type is a singleton (i.e. + RRsets of the type can have only one member.) + + *rdclass*, the rdataclass of the type, or ``dns.rdataclass.ANY`` if + it applies to all classes. + """ + + rdtype = dns.rdatatype.RdataType.make(rdtype) + existing_cls = get_rdata_class(rdclass, rdtype) + if existing_cls != GenericRdata or dns.rdatatype.is_metatype(rdtype): + raise RdatatypeExists(rdclass=rdclass, rdtype=rdtype) + if isinstance(implementation, type) and issubclass(implementation, Rdata): + impclass = implementation + else: + impclass = getattr(implementation, rdtype_text.replace("-", "_")) + _rdata_classes[(rdclass, rdtype)] = impclass + dns.rdatatype.register_type(rdtype, rdtype_text, is_singleton) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdataclass.py b/netdeploy/lib/python3.11/site-packages/dns/rdataclass.py new file mode 100644 index 0000000..89b85a7 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdataclass.py @@ -0,0 +1,118 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Rdata Classes.""" + +import dns.enum +import dns.exception + + +class RdataClass(dns.enum.IntEnum): + """DNS Rdata Class""" + + RESERVED0 = 0 + IN = 1 + INTERNET = IN + CH = 3 + CHAOS = CH + HS = 4 + HESIOD = HS + NONE = 254 + ANY = 255 + + @classmethod + def _maximum(cls): + return 65535 + + @classmethod + def _short_name(cls): + return "class" + + @classmethod + def _prefix(cls): + return "CLASS" + + @classmethod + def _unknown_exception_class(cls): + return UnknownRdataclass + + +_metaclasses = {RdataClass.NONE, RdataClass.ANY} + + +class UnknownRdataclass(dns.exception.DNSException): + """A DNS class is unknown.""" + + +def from_text(text: str) -> RdataClass: + """Convert text into a DNS rdata class value. + + The input text can be a defined DNS RR class mnemonic or + instance of the DNS generic class syntax. + + For example, "IN" and "CLASS1" will both result in a value of 1. + + Raises ``dns.rdatatype.UnknownRdataclass`` if the class is unknown. + + Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535. + + Returns a ``dns.rdataclass.RdataClass``. + """ + + return RdataClass.from_text(text) + + +def to_text(value: RdataClass) -> str: + """Convert a DNS rdata class value to text. + + If the value has a known mnemonic, it will be used, otherwise the + DNS generic class syntax will be used. + + Raises ``ValueError`` if the rdata class value is not >= 0 and <= 65535. + + Returns a ``str``. + """ + + return RdataClass.to_text(value) + + +def is_metaclass(rdclass: RdataClass) -> bool: + """True if the specified class is a metaclass. + + The currently defined metaclasses are ANY and NONE. + + *rdclass* is a ``dns.rdataclass.RdataClass``. + """ + + if rdclass in _metaclasses: + return True + return False + + +### BEGIN generated RdataClass constants + +RESERVED0 = RdataClass.RESERVED0 +IN = RdataClass.IN +INTERNET = RdataClass.INTERNET +CH = RdataClass.CH +CHAOS = RdataClass.CHAOS +HS = RdataClass.HS +HESIOD = RdataClass.HESIOD +NONE = RdataClass.NONE +ANY = RdataClass.ANY + +### END generated RdataClass constants diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdataset.py b/netdeploy/lib/python3.11/site-packages/dns/rdataset.py new file mode 100644 index 0000000..1edf67d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdataset.py @@ -0,0 +1,508 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS rdatasets (an rdataset is a set of rdatas of a given type and class)""" + +import io +import random +import struct +from typing import Any, Collection, Dict, List, cast + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.renderer +import dns.set +import dns.ttl + +# define SimpleSet here for backwards compatibility +SimpleSet = dns.set.Set + + +class DifferingCovers(dns.exception.DNSException): + """An attempt was made to add a DNS SIG/RRSIG whose covered type + is not the same as that of the other rdatas in the rdataset.""" + + +class IncompatibleTypes(dns.exception.DNSException): + """An attempt was made to add DNS RR data of an incompatible type.""" + + +class Rdataset(dns.set.Set): + """A DNS rdataset.""" + + __slots__ = ["rdclass", "rdtype", "covers", "ttl"] + + def __init__( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + ttl: int = 0, + ): + """Create a new rdataset of the specified class and type. + + *rdclass*, a ``dns.rdataclass.RdataClass``, the rdataclass. + + *rdtype*, an ``dns.rdatatype.RdataType``, the rdatatype. + + *covers*, an ``dns.rdatatype.RdataType``, the covered rdatatype. + + *ttl*, an ``int``, the TTL. + """ + + super().__init__() + self.rdclass = rdclass + self.rdtype: dns.rdatatype.RdataType = rdtype + self.covers: dns.rdatatype.RdataType = covers + self.ttl = ttl + + def _clone(self): + obj = cast(Rdataset, super()._clone()) + obj.rdclass = self.rdclass + obj.rdtype = self.rdtype + obj.covers = self.covers + obj.ttl = self.ttl + return obj + + def update_ttl(self, ttl: int) -> None: + """Perform TTL minimization. + + Set the TTL of the rdataset to be the lesser of the set's current + TTL or the specified TTL. If the set contains no rdatas, set the TTL + to the specified TTL. + + *ttl*, an ``int`` or ``str``. + """ + ttl = dns.ttl.make(ttl) + if len(self) == 0: + self.ttl = ttl + elif ttl < self.ttl: + self.ttl = ttl + + # pylint: disable=arguments-differ,arguments-renamed + def add( # pyright: ignore + self, rd: dns.rdata.Rdata, ttl: int | None = None + ) -> None: + """Add the specified rdata to the rdataset. + + If the optional *ttl* parameter is supplied, then + ``self.update_ttl(ttl)`` will be called prior to adding the rdata. + + *rd*, a ``dns.rdata.Rdata``, the rdata + + *ttl*, an ``int``, the TTL. + + Raises ``dns.rdataset.IncompatibleTypes`` if the type and class + do not match the type and class of the rdataset. + + Raises ``dns.rdataset.DifferingCovers`` if the type is a signature + type and the covered type does not match that of the rdataset. + """ + + # + # If we're adding a signature, do some special handling to + # check that the signature covers the same type as the + # other rdatas in this rdataset. If this is the first rdata + # in the set, initialize the covers field. + # + if self.rdclass != rd.rdclass or self.rdtype != rd.rdtype: + raise IncompatibleTypes + if ttl is not None: + self.update_ttl(ttl) + if self.rdtype == dns.rdatatype.RRSIG or self.rdtype == dns.rdatatype.SIG: + covers = rd.covers() + if len(self) == 0 and self.covers == dns.rdatatype.NONE: + self.covers = covers + elif self.covers != covers: + raise DifferingCovers + if dns.rdatatype.is_singleton(rd.rdtype) and len(self) > 0: + self.clear() + super().add(rd) + + def union_update(self, other): + self.update_ttl(other.ttl) + super().union_update(other) + + def intersection_update(self, other): + self.update_ttl(other.ttl) + super().intersection_update(other) + + def update(self, other): + """Add all rdatas in other to self. + + *other*, a ``dns.rdataset.Rdataset``, the rdataset from which + to update. + """ + + self.update_ttl(other.ttl) + super().update(other) + + def _rdata_repr(self): + def maybe_truncate(s): + if len(s) > 100: + return s[:100] + "..." + return s + + return "[" + ", ".join(f"<{maybe_truncate(str(rr))}>" for rr in self) + "]" + + def __repr__(self): + if self.covers == 0: + ctext = "" + else: + ctext = "(" + dns.rdatatype.to_text(self.covers) + ")" + return ( + "" + ) + + def __str__(self): + return self.to_text() + + def __eq__(self, other): + if not isinstance(other, Rdataset): + return False + if ( + self.rdclass != other.rdclass + or self.rdtype != other.rdtype + or self.covers != other.covers + ): + return False + return super().__eq__(other) + + def __ne__(self, other): + return not self.__eq__(other) + + def to_text( + self, + name: dns.name.Name | None = None, + origin: dns.name.Name | None = None, + relativize: bool = True, + override_rdclass: dns.rdataclass.RdataClass | None = None, + want_comments: bool = False, + **kw: Dict[str, Any], + ) -> str: + """Convert the rdataset into DNS zone file format. + + See ``dns.name.Name.choose_relativity`` for more information + on how *origin* and *relativize* determine the way names + are emitted. + + Any additional keyword arguments are passed on to the rdata + ``to_text()`` method. + + *name*, a ``dns.name.Name``. If name is not ``None``, emit RRs with + *name* as the owner name. + + *origin*, a ``dns.name.Name`` or ``None``, the origin for relative + names. + + *relativize*, a ``bool``. If ``True``, names will be relativized + to *origin*. + + *override_rdclass*, a ``dns.rdataclass.RdataClass`` or ``None``. + If not ``None``, use this class instead of the Rdataset's class. + + *want_comments*, a ``bool``. If ``True``, emit comments for rdata + which have them. The default is ``False``. + """ + + if name is not None: + name = name.choose_relativity(origin, relativize) + ntext = str(name) + pad = " " + else: + ntext = "" + pad = "" + s = io.StringIO() + if override_rdclass is not None: + rdclass = override_rdclass + else: + rdclass = self.rdclass + if len(self) == 0: + # + # Empty rdatasets are used for the question section, and in + # some dynamic updates, so we don't need to print out the TTL + # (which is meaningless anyway). + # + s.write( + f"{ntext}{pad}{dns.rdataclass.to_text(rdclass)} " + f"{dns.rdatatype.to_text(self.rdtype)}\n" + ) + else: + for rd in self: + extra = "" + if want_comments: + if rd.rdcomment: + extra = f" ;{rd.rdcomment}" + s.write( + f"{ntext}{pad}{self.ttl} " + f"{dns.rdataclass.to_text(rdclass)} " + f"{dns.rdatatype.to_text(self.rdtype)} " + f"{rd.to_text(origin=origin, relativize=relativize, **kw)}" + f"{extra}\n" + ) + # + # We strip off the final \n for the caller's convenience in printing + # + return s.getvalue()[:-1] + + def to_wire( + self, + name: dns.name.Name, + file: Any, + compress: dns.name.CompressType | None = None, + origin: dns.name.Name | None = None, + override_rdclass: dns.rdataclass.RdataClass | None = None, + want_shuffle: bool = True, + ) -> int: + """Convert the rdataset to wire format. + + *name*, a ``dns.name.Name`` is the owner name to use. + + *file* is the file where the name is emitted (typically a + BytesIO file). + + *compress*, a ``dict``, is the compression table to use. If + ``None`` (the default), names will not be compressed. + + *origin* is a ``dns.name.Name`` or ``None``. If the name is + relative and origin is not ``None``, then *origin* will be appended + to it. + + *override_rdclass*, an ``int``, is used as the class instead of the + class of the rdataset. This is useful when rendering rdatasets + associated with dynamic updates. + + *want_shuffle*, a ``bool``. If ``True``, then the order of the + Rdatas within the Rdataset will be shuffled before rendering. + + Returns an ``int``, the number of records emitted. + """ + + if override_rdclass is not None: + rdclass = override_rdclass + want_shuffle = False + else: + rdclass = self.rdclass + if len(self) == 0: + name.to_wire(file, compress, origin) + file.write(struct.pack("!HHIH", self.rdtype, rdclass, 0, 0)) + return 1 + else: + l: Rdataset | List[dns.rdata.Rdata] + if want_shuffle: + l = list(self) + random.shuffle(l) + else: + l = self + for rd in l: + name.to_wire(file, compress, origin) + file.write(struct.pack("!HHI", self.rdtype, rdclass, self.ttl)) + with dns.renderer.prefixed_length(file, 2): + rd.to_wire(file, compress, origin) + return len(self) + + def match( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType, + ) -> bool: + """Returns ``True`` if this rdataset matches the specified class, + type, and covers. + """ + if self.rdclass == rdclass and self.rdtype == rdtype and self.covers == covers: + return True + return False + + def processing_order(self) -> List[dns.rdata.Rdata]: + """Return rdatas in a valid processing order according to the type's + specification. For example, MX records are in preference order from + lowest to highest preferences, with items of the same preference + shuffled. + + For types that do not define a processing order, the rdatas are + simply shuffled. + """ + if len(self) == 0: + return [] + else: + return self[0]._processing_order(iter(self)) # pyright: ignore + + +@dns.immutable.immutable +class ImmutableRdataset(Rdataset): # lgtm[py/missing-equals] + """An immutable DNS rdataset.""" + + _clone_class = Rdataset + + def __init__(self, rdataset: Rdataset): + """Create an immutable rdataset from the specified rdataset.""" + + super().__init__( + rdataset.rdclass, rdataset.rdtype, rdataset.covers, rdataset.ttl + ) + self.items = dns.immutable.Dict(rdataset.items) + + def update_ttl(self, ttl): + raise TypeError("immutable") + + def add(self, rd, ttl=None): + raise TypeError("immutable") + + def union_update(self, other): + raise TypeError("immutable") + + def intersection_update(self, other): + raise TypeError("immutable") + + def update(self, other): + raise TypeError("immutable") + + def __delitem__(self, i): + raise TypeError("immutable") + + # lgtm complains about these not raising ArithmeticError, but there is + # precedent for overrides of these methods in other classes to raise + # TypeError, and it seems like the better exception. + + def __ior__(self, other): # lgtm[py/unexpected-raise-in-special-method] + raise TypeError("immutable") + + def __iand__(self, other): # lgtm[py/unexpected-raise-in-special-method] + raise TypeError("immutable") + + def __iadd__(self, other): # lgtm[py/unexpected-raise-in-special-method] + raise TypeError("immutable") + + def __isub__(self, other): # lgtm[py/unexpected-raise-in-special-method] + raise TypeError("immutable") + + def clear(self): + raise TypeError("immutable") + + def __copy__(self): + return ImmutableRdataset(super().copy()) # pyright: ignore + + def copy(self): + return ImmutableRdataset(super().copy()) # pyright: ignore + + def union(self, other): + return ImmutableRdataset(super().union(other)) # pyright: ignore + + def intersection(self, other): + return ImmutableRdataset(super().intersection(other)) # pyright: ignore + + def difference(self, other): + return ImmutableRdataset(super().difference(other)) # pyright: ignore + + def symmetric_difference(self, other): + return ImmutableRdataset(super().symmetric_difference(other)) # pyright: ignore + + +def from_text_list( + rdclass: dns.rdataclass.RdataClass | str, + rdtype: dns.rdatatype.RdataType | str, + ttl: int, + text_rdatas: Collection[str], + idna_codec: dns.name.IDNACodec | None = None, + origin: dns.name.Name | None = None, + relativize: bool = True, + relativize_to: dns.name.Name | None = None, +) -> Rdataset: + """Create an rdataset with the specified class, type, and TTL, and with + the specified list of rdatas in text format. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder to use; if ``None``, the default IDNA 2003 + encoder/decoder is used. + + *origin*, a ``dns.name.Name`` (or ``None``), the + origin to use for relative names. + + *relativize*, a ``bool``. If true, name will be relativized. + + *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use + when relativizing names. If not set, the *origin* value will be used. + + Returns a ``dns.rdataset.Rdataset`` object. + """ + + rdclass = dns.rdataclass.RdataClass.make(rdclass) + rdtype = dns.rdatatype.RdataType.make(rdtype) + r = Rdataset(rdclass, rdtype) + r.update_ttl(ttl) + for t in text_rdatas: + rd = dns.rdata.from_text( + r.rdclass, r.rdtype, t, origin, relativize, relativize_to, idna_codec + ) + r.add(rd) + return r + + +def from_text( + rdclass: dns.rdataclass.RdataClass | str, + rdtype: dns.rdatatype.RdataType | str, + ttl: int, + *text_rdatas: Any, +) -> Rdataset: + """Create an rdataset with the specified class, type, and TTL, and with + the specified rdatas in text format. + + Returns a ``dns.rdataset.Rdataset`` object. + """ + + return from_text_list(rdclass, rdtype, ttl, cast(Collection[str], text_rdatas)) + + +def from_rdata_list(ttl: int, rdatas: Collection[dns.rdata.Rdata]) -> Rdataset: + """Create an rdataset with the specified TTL, and with + the specified list of rdata objects. + + Returns a ``dns.rdataset.Rdataset`` object. + """ + + if len(rdatas) == 0: + raise ValueError("rdata list must not be empty") + r = None + for rd in rdatas: + if r is None: + r = Rdataset(rd.rdclass, rd.rdtype) + r.update_ttl(ttl) + r.add(rd) + assert r is not None + return r + + +def from_rdata(ttl: int, *rdatas: Any) -> Rdataset: + """Create an rdataset with the specified TTL, and with + the specified rdata objects. + + Returns a ``dns.rdataset.Rdataset`` object. + """ + + return from_rdata_list(ttl, cast(Collection[dns.rdata.Rdata], rdatas)) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdatatype.py b/netdeploy/lib/python3.11/site-packages/dns/rdatatype.py new file mode 100644 index 0000000..211d810 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdatatype.py @@ -0,0 +1,338 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Rdata Types.""" + +from typing import Dict + +import dns.enum +import dns.exception + + +class RdataType(dns.enum.IntEnum): + """DNS Rdata Type""" + + TYPE0 = 0 + NONE = 0 + A = 1 + NS = 2 + MD = 3 + MF = 4 + CNAME = 5 + SOA = 6 + MB = 7 + MG = 8 + MR = 9 + NULL = 10 + WKS = 11 + PTR = 12 + HINFO = 13 + MINFO = 14 + MX = 15 + TXT = 16 + RP = 17 + AFSDB = 18 + X25 = 19 + ISDN = 20 + RT = 21 + NSAP = 22 + NSAP_PTR = 23 + SIG = 24 + KEY = 25 + PX = 26 + GPOS = 27 + AAAA = 28 + LOC = 29 + NXT = 30 + SRV = 33 + NAPTR = 35 + KX = 36 + CERT = 37 + A6 = 38 + DNAME = 39 + OPT = 41 + APL = 42 + DS = 43 + SSHFP = 44 + IPSECKEY = 45 + RRSIG = 46 + NSEC = 47 + DNSKEY = 48 + DHCID = 49 + NSEC3 = 50 + NSEC3PARAM = 51 + TLSA = 52 + SMIMEA = 53 + HIP = 55 + NINFO = 56 + CDS = 59 + CDNSKEY = 60 + OPENPGPKEY = 61 + CSYNC = 62 + ZONEMD = 63 + SVCB = 64 + HTTPS = 65 + DSYNC = 66 + SPF = 99 + UNSPEC = 103 + NID = 104 + L32 = 105 + L64 = 106 + LP = 107 + EUI48 = 108 + EUI64 = 109 + TKEY = 249 + TSIG = 250 + IXFR = 251 + AXFR = 252 + MAILB = 253 + MAILA = 254 + ANY = 255 + URI = 256 + CAA = 257 + AVC = 258 + AMTRELAY = 260 + RESINFO = 261 + WALLET = 262 + TA = 32768 + DLV = 32769 + + @classmethod + def _maximum(cls): + return 65535 + + @classmethod + def _short_name(cls): + return "type" + + @classmethod + def _prefix(cls): + return "TYPE" + + @classmethod + def _extra_from_text(cls, text): + if text.find("-") >= 0: + try: + return cls[text.replace("-", "_")] + except KeyError: # pragma: no cover + pass + return _registered_by_text.get(text) + + @classmethod + def _extra_to_text(cls, value, current_text): + if current_text is None: + return _registered_by_value.get(value) + if current_text.find("_") >= 0: + return current_text.replace("_", "-") + return current_text + + @classmethod + def _unknown_exception_class(cls): + return UnknownRdatatype + + +_registered_by_text: Dict[str, RdataType] = {} +_registered_by_value: Dict[RdataType, str] = {} + +_metatypes = {RdataType.OPT} + +_singletons = { + RdataType.SOA, + RdataType.NXT, + RdataType.DNAME, + RdataType.NSEC, + RdataType.CNAME, +} + + +class UnknownRdatatype(dns.exception.DNSException): + """DNS resource record type is unknown.""" + + +def from_text(text: str) -> RdataType: + """Convert text into a DNS rdata type value. + + The input text can be a defined DNS RR type mnemonic or + instance of the DNS generic type syntax. + + For example, "NS" and "TYPE2" will both result in a value of 2. + + Raises ``dns.rdatatype.UnknownRdatatype`` if the type is unknown. + + Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535. + + Returns a ``dns.rdatatype.RdataType``. + """ + + return RdataType.from_text(text) + + +def to_text(value: RdataType) -> str: + """Convert a DNS rdata type value to text. + + If the value has a known mnemonic, it will be used, otherwise the + DNS generic type syntax will be used. + + Raises ``ValueError`` if the rdata type value is not >= 0 and <= 65535. + + Returns a ``str``. + """ + + return RdataType.to_text(value) + + +def is_metatype(rdtype: RdataType) -> bool: + """True if the specified type is a metatype. + + *rdtype* is a ``dns.rdatatype.RdataType``. + + The currently defined metatypes are TKEY, TSIG, IXFR, AXFR, MAILA, + MAILB, ANY, and OPT. + + Returns a ``bool``. + """ + + return (256 > rdtype >= 128) or rdtype in _metatypes + + +def is_singleton(rdtype: RdataType) -> bool: + """Is the specified type a singleton type? + + Singleton types can only have a single rdata in an rdataset, or a single + RR in an RRset. + + The currently defined singleton types are CNAME, DNAME, NSEC, NXT, and + SOA. + + *rdtype* is an ``int``. + + Returns a ``bool``. + """ + + if rdtype in _singletons: + return True + return False + + +# pylint: disable=redefined-outer-name +def register_type( + rdtype: RdataType, rdtype_text: str, is_singleton: bool = False +) -> None: + """Dynamically register an rdatatype. + + *rdtype*, a ``dns.rdatatype.RdataType``, the rdatatype to register. + + *rdtype_text*, a ``str``, the textual form of the rdatatype. + + *is_singleton*, a ``bool``, indicating if the type is a singleton (i.e. + RRsets of the type can have only one member.) + """ + + _registered_by_text[rdtype_text] = rdtype + _registered_by_value[rdtype] = rdtype_text + if is_singleton: + _singletons.add(rdtype) + + +### BEGIN generated RdataType constants + +TYPE0 = RdataType.TYPE0 +NONE = RdataType.NONE +A = RdataType.A +NS = RdataType.NS +MD = RdataType.MD +MF = RdataType.MF +CNAME = RdataType.CNAME +SOA = RdataType.SOA +MB = RdataType.MB +MG = RdataType.MG +MR = RdataType.MR +NULL = RdataType.NULL +WKS = RdataType.WKS +PTR = RdataType.PTR +HINFO = RdataType.HINFO +MINFO = RdataType.MINFO +MX = RdataType.MX +TXT = RdataType.TXT +RP = RdataType.RP +AFSDB = RdataType.AFSDB +X25 = RdataType.X25 +ISDN = RdataType.ISDN +RT = RdataType.RT +NSAP = RdataType.NSAP +NSAP_PTR = RdataType.NSAP_PTR +SIG = RdataType.SIG +KEY = RdataType.KEY +PX = RdataType.PX +GPOS = RdataType.GPOS +AAAA = RdataType.AAAA +LOC = RdataType.LOC +NXT = RdataType.NXT +SRV = RdataType.SRV +NAPTR = RdataType.NAPTR +KX = RdataType.KX +CERT = RdataType.CERT +A6 = RdataType.A6 +DNAME = RdataType.DNAME +OPT = RdataType.OPT +APL = RdataType.APL +DS = RdataType.DS +SSHFP = RdataType.SSHFP +IPSECKEY = RdataType.IPSECKEY +RRSIG = RdataType.RRSIG +NSEC = RdataType.NSEC +DNSKEY = RdataType.DNSKEY +DHCID = RdataType.DHCID +NSEC3 = RdataType.NSEC3 +NSEC3PARAM = RdataType.NSEC3PARAM +TLSA = RdataType.TLSA +SMIMEA = RdataType.SMIMEA +HIP = RdataType.HIP +NINFO = RdataType.NINFO +CDS = RdataType.CDS +CDNSKEY = RdataType.CDNSKEY +OPENPGPKEY = RdataType.OPENPGPKEY +CSYNC = RdataType.CSYNC +ZONEMD = RdataType.ZONEMD +SVCB = RdataType.SVCB +HTTPS = RdataType.HTTPS +DSYNC = RdataType.DSYNC +SPF = RdataType.SPF +UNSPEC = RdataType.UNSPEC +NID = RdataType.NID +L32 = RdataType.L32 +L64 = RdataType.L64 +LP = RdataType.LP +EUI48 = RdataType.EUI48 +EUI64 = RdataType.EUI64 +TKEY = RdataType.TKEY +TSIG = RdataType.TSIG +IXFR = RdataType.IXFR +AXFR = RdataType.AXFR +MAILB = RdataType.MAILB +MAILA = RdataType.MAILA +ANY = RdataType.ANY +URI = RdataType.URI +CAA = RdataType.CAA +AVC = RdataType.AVC +AMTRELAY = RdataType.AMTRELAY +RESINFO = RdataType.RESINFO +WALLET = RdataType.WALLET +TA = RdataType.TA +DLV = RdataType.DLV + +### END generated RdataType constants diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AFSDB.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AFSDB.py new file mode 100644 index 0000000..06a3b97 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AFSDB.py @@ -0,0 +1,45 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.mxbase + + +@dns.immutable.immutable +class AFSDB(dns.rdtypes.mxbase.UncompressedDowncasingMX): + """AFSDB record""" + + # Use the property mechanism to make "subtype" an alias for the + # "preference" attribute, and "hostname" an alias for the "exchange" + # attribute. + # + # This lets us inherit the UncompressedMX implementation but lets + # the caller use appropriate attribute names for the rdata type. + # + # We probably lose some performance vs. a cut-and-paste + # implementation, but this way we don't copy code, and that's + # good. + + @property + def subtype(self): + "the AFSDB subtype" + return self.preference + + @property + def hostname(self): + "the AFSDB hostname" + return self.exchange diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AMTRELAY.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AMTRELAY.py new file mode 100644 index 0000000..dc9fa87 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AMTRELAY.py @@ -0,0 +1,89 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.rdata +import dns.rdtypes.util + + +class Relay(dns.rdtypes.util.Gateway): + name = "AMTRELAY relay" + + @property + def relay(self): + return self.gateway + + +@dns.immutable.immutable +class AMTRELAY(dns.rdata.Rdata): + """AMTRELAY record""" + + # see: RFC 8777 + + __slots__ = ["precedence", "discovery_optional", "relay_type", "relay"] + + def __init__( + self, rdclass, rdtype, precedence, discovery_optional, relay_type, relay + ): + super().__init__(rdclass, rdtype) + relay = Relay(relay_type, relay) + self.precedence = self._as_uint8(precedence) + self.discovery_optional = self._as_bool(discovery_optional) + self.relay_type = relay.type + self.relay = relay.relay + + def to_text(self, origin=None, relativize=True, **kw): + relay = Relay(self.relay_type, self.relay).to_text(origin, relativize) + return ( + f"{self.precedence} {self.discovery_optional:d} {self.relay_type} {relay}" + ) + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + precedence = tok.get_uint8() + discovery_optional = tok.get_uint8() + if discovery_optional > 1: + raise dns.exception.SyntaxError("expecting 0 or 1") + discovery_optional = bool(discovery_optional) + relay_type = tok.get_uint8() + if relay_type > 0x7F: + raise dns.exception.SyntaxError("expecting an integer <= 127") + relay = Relay.from_text(relay_type, tok, origin, relativize, relativize_to) + return cls( + rdclass, rdtype, precedence, discovery_optional, relay_type, relay.relay + ) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + relay_type = self.relay_type | (self.discovery_optional << 7) + header = struct.pack("!BB", self.precedence, relay_type) + file.write(header) + Relay(self.relay_type, self.relay).to_wire(file, compress, origin, canonicalize) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (precedence, relay_type) = parser.get_struct("!BB") + discovery_optional = bool(relay_type >> 7) + relay_type &= 0x7F + relay = Relay.from_wire_parser(relay_type, parser, origin) + return cls( + rdclass, rdtype, precedence, discovery_optional, relay_type, relay.relay + ) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AVC.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AVC.py new file mode 100644 index 0000000..a27ae2d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/AVC.py @@ -0,0 +1,26 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2016 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.txtbase + + +@dns.immutable.immutable +class AVC(dns.rdtypes.txtbase.TXTBase): + """AVC record""" + + # See: IANA dns parameters for AVC diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CAA.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CAA.py new file mode 100644 index 0000000..8c62e62 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CAA.py @@ -0,0 +1,67 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.rdata +import dns.tokenizer + + +@dns.immutable.immutable +class CAA(dns.rdata.Rdata): + """CAA (Certification Authority Authorization) record""" + + # see: RFC 6844 + + __slots__ = ["flags", "tag", "value"] + + def __init__(self, rdclass, rdtype, flags, tag, value): + super().__init__(rdclass, rdtype) + self.flags = self._as_uint8(flags) + self.tag = self._as_bytes(tag, True, 255) + if not tag.isalnum(): + raise ValueError("tag is not alphanumeric") + self.value = self._as_bytes(value) + + def to_text(self, origin=None, relativize=True, **kw): + return f'{self.flags} {dns.rdata._escapify(self.tag)} "{dns.rdata._escapify(self.value)}"' + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + flags = tok.get_uint8() + tag = tok.get_string().encode() + value = tok.get_string().encode() + return cls(rdclass, rdtype, flags, tag, value) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(struct.pack("!B", self.flags)) + l = len(self.tag) + assert l < 256 + file.write(struct.pack("!B", l)) + file.write(self.tag) + file.write(self.value) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + flags = parser.get_uint8() + tag = parser.get_counted_bytes() + value = parser.get_remaining() + return cls(rdclass, rdtype, flags, tag, value) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CDNSKEY.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CDNSKEY.py new file mode 100644 index 0000000..b613409 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CDNSKEY.py @@ -0,0 +1,33 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.dnskeybase # lgtm[py/import-and-import-from] + +# pylint: disable=unused-import +from dns.rdtypes.dnskeybase import ( # noqa: F401 lgtm[py/unused-import] + REVOKE, + SEP, + ZONE, +) + +# pylint: enable=unused-import + + +@dns.immutable.immutable +class CDNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase): + """CDNSKEY record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CDS.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CDS.py new file mode 100644 index 0000000..8312b97 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CDS.py @@ -0,0 +1,29 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.dsbase + + +@dns.immutable.immutable +class CDS(dns.rdtypes.dsbase.DSBase): + """CDS record""" + + _digest_length_by_type = { + **dns.rdtypes.dsbase.DSBase._digest_length_by_type, + 0: 1, # delete, RFC 8078 Sec. 4 (including Errata ID 5049) + } diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CERT.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CERT.py new file mode 100644 index 0000000..4d5e5bd --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CERT.py @@ -0,0 +1,113 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 +import struct + +import dns.dnssectypes +import dns.exception +import dns.immutable +import dns.rdata +import dns.tokenizer + +_ctype_by_value = { + 1: "PKIX", + 2: "SPKI", + 3: "PGP", + 4: "IPKIX", + 5: "ISPKI", + 6: "IPGP", + 7: "ACPKIX", + 8: "IACPKIX", + 253: "URI", + 254: "OID", +} + +_ctype_by_name = { + "PKIX": 1, + "SPKI": 2, + "PGP": 3, + "IPKIX": 4, + "ISPKI": 5, + "IPGP": 6, + "ACPKIX": 7, + "IACPKIX": 8, + "URI": 253, + "OID": 254, +} + + +def _ctype_from_text(what): + v = _ctype_by_name.get(what) + if v is not None: + return v + return int(what) + + +def _ctype_to_text(what): + v = _ctype_by_value.get(what) + if v is not None: + return v + return str(what) + + +@dns.immutable.immutable +class CERT(dns.rdata.Rdata): + """CERT record""" + + # see RFC 4398 + + __slots__ = ["certificate_type", "key_tag", "algorithm", "certificate"] + + def __init__( + self, rdclass, rdtype, certificate_type, key_tag, algorithm, certificate + ): + super().__init__(rdclass, rdtype) + self.certificate_type = self._as_uint16(certificate_type) + self.key_tag = self._as_uint16(key_tag) + self.algorithm = self._as_uint8(algorithm) + self.certificate = self._as_bytes(certificate) + + def to_text(self, origin=None, relativize=True, **kw): + certificate_type = _ctype_to_text(self.certificate_type) + algorithm = dns.dnssectypes.Algorithm.to_text(self.algorithm) + certificate = dns.rdata._base64ify(self.certificate, **kw) # pyright: ignore + return f"{certificate_type} {self.key_tag} {algorithm} {certificate}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + certificate_type = _ctype_from_text(tok.get_string()) + key_tag = tok.get_uint16() + algorithm = dns.dnssectypes.Algorithm.from_text(tok.get_string()) + b64 = tok.concatenate_remaining_identifiers().encode() + certificate = base64.b64decode(b64) + return cls(rdclass, rdtype, certificate_type, key_tag, algorithm, certificate) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + prefix = struct.pack( + "!HHB", self.certificate_type, self.key_tag, self.algorithm + ) + file.write(prefix) + file.write(self.certificate) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (certificate_type, key_tag, algorithm) = parser.get_struct("!HHB") + certificate = parser.get_remaining() + return cls(rdclass, rdtype, certificate_type, key_tag, algorithm, certificate) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CNAME.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CNAME.py new file mode 100644 index 0000000..665e407 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CNAME.py @@ -0,0 +1,28 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.nsbase + + +@dns.immutable.immutable +class CNAME(dns.rdtypes.nsbase.NSBase): + """CNAME record + + Note: although CNAME is officially a singleton type, dnspython allows + non-singleton CNAME rdatasets because such sets have been commonly + used by BIND and other nameservers for load balancing.""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CSYNC.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CSYNC.py new file mode 100644 index 0000000..103486d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/CSYNC.py @@ -0,0 +1,68 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011, 2016 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata +import dns.rdatatype +import dns.rdtypes.util + + +@dns.immutable.immutable +class Bitmap(dns.rdtypes.util.Bitmap): + type_name = "CSYNC" + + +@dns.immutable.immutable +class CSYNC(dns.rdata.Rdata): + """CSYNC record""" + + __slots__ = ["serial", "flags", "windows"] + + def __init__(self, rdclass, rdtype, serial, flags, windows): + super().__init__(rdclass, rdtype) + self.serial = self._as_uint32(serial) + self.flags = self._as_uint16(flags) + if not isinstance(windows, Bitmap): + windows = Bitmap(windows) + self.windows = tuple(windows.windows) + + def to_text(self, origin=None, relativize=True, **kw): + text = Bitmap(self.windows).to_text() + return f"{self.serial} {self.flags}{text}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + serial = tok.get_uint32() + flags = tok.get_uint16() + bitmap = Bitmap.from_text(tok) + return cls(rdclass, rdtype, serial, flags, bitmap) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(struct.pack("!IH", self.serial, self.flags)) + Bitmap(self.windows).to_wire(file) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (serial, flags) = parser.get_struct("!IH") + bitmap = Bitmap.from_wire_parser(parser) + return cls(rdclass, rdtype, serial, flags, bitmap) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DLV.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DLV.py new file mode 100644 index 0000000..6c134f1 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DLV.py @@ -0,0 +1,24 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.dsbase + + +@dns.immutable.immutable +class DLV(dns.rdtypes.dsbase.DSBase): + """DLV record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DNAME.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DNAME.py new file mode 100644 index 0000000..bbf9186 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DNAME.py @@ -0,0 +1,27 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.nsbase + + +@dns.immutable.immutable +class DNAME(dns.rdtypes.nsbase.UncompressedNS): + """DNAME record""" + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.target.to_wire(file, None, origin, canonicalize) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DNSKEY.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DNSKEY.py new file mode 100644 index 0000000..6d961a9 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DNSKEY.py @@ -0,0 +1,33 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.dnskeybase # lgtm[py/import-and-import-from] + +# pylint: disable=unused-import +from dns.rdtypes.dnskeybase import ( # noqa: F401 lgtm[py/unused-import] + REVOKE, + SEP, + ZONE, +) + +# pylint: enable=unused-import + + +@dns.immutable.immutable +class DNSKEY(dns.rdtypes.dnskeybase.DNSKEYBase): + """DNSKEY record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DS.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DS.py new file mode 100644 index 0000000..58b3108 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DS.py @@ -0,0 +1,24 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.dsbase + + +@dns.immutable.immutable +class DS(dns.rdtypes.dsbase.DSBase): + """DS record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DSYNC.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DSYNC.py new file mode 100644 index 0000000..e8d1394 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/DSYNC.py @@ -0,0 +1,72 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.enum +import dns.exception +import dns.immutable +import dns.rdata +import dns.rdatatype +import dns.rdtypes.util + + +class UnknownScheme(dns.exception.DNSException): + """Unknown DSYNC scheme""" + + +class Scheme(dns.enum.IntEnum): + """DSYNC SCHEME""" + + NOTIFY = 1 + + @classmethod + def _maximum(cls): + return 255 + + @classmethod + def _unknown_exception_class(cls): + return UnknownScheme + + +@dns.immutable.immutable +class DSYNC(dns.rdata.Rdata): + """DSYNC record""" + + # see: draft-ietf-dnsop-generalized-notify + + __slots__ = ["rrtype", "scheme", "port", "target"] + + def __init__(self, rdclass, rdtype, rrtype, scheme, port, target): + super().__init__(rdclass, rdtype) + self.rrtype = self._as_rdatatype(rrtype) + self.scheme = Scheme.make(scheme) + self.port = self._as_uint16(port) + self.target = self._as_name(target) + + def to_text(self, origin=None, relativize=True, **kw): + target = self.target.choose_relativity(origin, relativize) + return ( + f"{dns.rdatatype.to_text(self.rrtype)} {Scheme.to_text(self.scheme)} " + f"{self.port} {target}" + ) + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + rrtype = dns.rdatatype.from_text(tok.get_string()) + scheme = Scheme.make(tok.get_string()) + port = tok.get_uint16() + target = tok.get_name(origin, relativize, relativize_to) + return cls(rdclass, rdtype, rrtype, scheme, port, target) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + three_ints = struct.pack("!HBH", self.rrtype, self.scheme, self.port) + file.write(three_ints) + self.target.to_wire(file, None, origin, False) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (rrtype, scheme, port) = parser.get_struct("!HBH") + target = parser.get_name(origin) + return cls(rdclass, rdtype, rrtype, scheme, port, target) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI48.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI48.py new file mode 100644 index 0000000..c843be5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI48.py @@ -0,0 +1,30 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2015 Red Hat, Inc. +# Author: Petr Spacek +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.euibase + + +@dns.immutable.immutable +class EUI48(dns.rdtypes.euibase.EUIBase): + """EUI48 record""" + + # see: rfc7043.txt + + byte_len = 6 # 0123456789ab (in hex) + text_len = byte_len * 3 - 1 # 01-23-45-67-89-ab diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI64.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI64.py new file mode 100644 index 0000000..f6d7e25 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI64.py @@ -0,0 +1,30 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2015 Red Hat, Inc. +# Author: Petr Spacek +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.euibase + + +@dns.immutable.immutable +class EUI64(dns.rdtypes.euibase.EUIBase): + """EUI64 record""" + + # see: rfc7043.txt + + byte_len = 8 # 0123456789abcdef (in hex) + text_len = byte_len * 3 - 1 # 01-23-45-67-89-ab-cd-ef diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/GPOS.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/GPOS.py new file mode 100644 index 0000000..d79f4a0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/GPOS.py @@ -0,0 +1,126 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.rdata +import dns.tokenizer + + +def _validate_float_string(what): + if len(what) == 0: + raise dns.exception.FormError + if what[0] == b"-"[0] or what[0] == b"+"[0]: + what = what[1:] + if what.isdigit(): + return + try: + (left, right) = what.split(b".") + except ValueError: + raise dns.exception.FormError + if left == b"" and right == b"": + raise dns.exception.FormError + if not left == b"" and not left.decode().isdigit(): + raise dns.exception.FormError + if not right == b"" and not right.decode().isdigit(): + raise dns.exception.FormError + + +@dns.immutable.immutable +class GPOS(dns.rdata.Rdata): + """GPOS record""" + + # see: RFC 1712 + + __slots__ = ["latitude", "longitude", "altitude"] + + def __init__(self, rdclass, rdtype, latitude, longitude, altitude): + super().__init__(rdclass, rdtype) + if isinstance(latitude, float) or isinstance(latitude, int): + latitude = str(latitude) + if isinstance(longitude, float) or isinstance(longitude, int): + longitude = str(longitude) + if isinstance(altitude, float) or isinstance(altitude, int): + altitude = str(altitude) + latitude = self._as_bytes(latitude, True, 255) + longitude = self._as_bytes(longitude, True, 255) + altitude = self._as_bytes(altitude, True, 255) + _validate_float_string(latitude) + _validate_float_string(longitude) + _validate_float_string(altitude) + self.latitude = latitude + self.longitude = longitude + self.altitude = altitude + flat = self.float_latitude + if flat < -90.0 or flat > 90.0: + raise dns.exception.FormError("bad latitude") + flong = self.float_longitude + if flong < -180.0 or flong > 180.0: + raise dns.exception.FormError("bad longitude") + + def to_text(self, origin=None, relativize=True, **kw): + return ( + f"{self.latitude.decode()} {self.longitude.decode()} " + f"{self.altitude.decode()}" + ) + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + latitude = tok.get_string() + longitude = tok.get_string() + altitude = tok.get_string() + return cls(rdclass, rdtype, latitude, longitude, altitude) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + l = len(self.latitude) + assert l < 256 + file.write(struct.pack("!B", l)) + file.write(self.latitude) + l = len(self.longitude) + assert l < 256 + file.write(struct.pack("!B", l)) + file.write(self.longitude) + l = len(self.altitude) + assert l < 256 + file.write(struct.pack("!B", l)) + file.write(self.altitude) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + latitude = parser.get_counted_bytes() + longitude = parser.get_counted_bytes() + altitude = parser.get_counted_bytes() + return cls(rdclass, rdtype, latitude, longitude, altitude) + + @property + def float_latitude(self): + "latitude as a floating point value" + return float(self.latitude) + + @property + def float_longitude(self): + "longitude as a floating point value" + return float(self.longitude) + + @property + def float_altitude(self): + "altitude as a floating point value" + return float(self.altitude) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/HINFO.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/HINFO.py new file mode 100644 index 0000000..06ad348 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/HINFO.py @@ -0,0 +1,64 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.rdata +import dns.tokenizer + + +@dns.immutable.immutable +class HINFO(dns.rdata.Rdata): + """HINFO record""" + + # see: RFC 1035 + + __slots__ = ["cpu", "os"] + + def __init__(self, rdclass, rdtype, cpu, os): + super().__init__(rdclass, rdtype) + self.cpu = self._as_bytes(cpu, True, 255) + self.os = self._as_bytes(os, True, 255) + + def to_text(self, origin=None, relativize=True, **kw): + return f'"{dns.rdata._escapify(self.cpu)}" "{dns.rdata._escapify(self.os)}"' + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + cpu = tok.get_string(max_length=255) + os = tok.get_string(max_length=255) + return cls(rdclass, rdtype, cpu, os) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + l = len(self.cpu) + assert l < 256 + file.write(struct.pack("!B", l)) + file.write(self.cpu) + l = len(self.os) + assert l < 256 + file.write(struct.pack("!B", l)) + file.write(self.os) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + cpu = parser.get_counted_bytes() + os = parser.get_counted_bytes() + return cls(rdclass, rdtype, cpu, os) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/HIP.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/HIP.py new file mode 100644 index 0000000..dc7948a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/HIP.py @@ -0,0 +1,85 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2010, 2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 +import binascii +import struct + +import dns.exception +import dns.immutable +import dns.rdata +import dns.rdatatype + + +@dns.immutable.immutable +class HIP(dns.rdata.Rdata): + """HIP record""" + + # see: RFC 5205 + + __slots__ = ["hit", "algorithm", "key", "servers"] + + def __init__(self, rdclass, rdtype, hit, algorithm, key, servers): + super().__init__(rdclass, rdtype) + self.hit = self._as_bytes(hit, True, 255) + self.algorithm = self._as_uint8(algorithm) + self.key = self._as_bytes(key, True) + self.servers = self._as_tuple(servers, self._as_name) + + def to_text(self, origin=None, relativize=True, **kw): + hit = binascii.hexlify(self.hit).decode() + key = base64.b64encode(self.key).replace(b"\n", b"").decode() + text = "" + servers = [] + for server in self.servers: + servers.append(server.choose_relativity(origin, relativize)) + if len(servers) > 0: + text += " " + " ".join(x.to_unicode() for x in servers) + return f"{self.algorithm} {hit} {key}{text}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + algorithm = tok.get_uint8() + hit = binascii.unhexlify(tok.get_string().encode()) + key = base64.b64decode(tok.get_string().encode()) + servers = [] + for token in tok.get_remaining(): + server = tok.as_name(token, origin, relativize, relativize_to) + servers.append(server) + return cls(rdclass, rdtype, hit, algorithm, key, servers) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + lh = len(self.hit) + lk = len(self.key) + file.write(struct.pack("!BBH", lh, self.algorithm, lk)) + file.write(self.hit) + file.write(self.key) + for server in self.servers: + server.to_wire(file, None, origin, False) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (lh, algorithm, lk) = parser.get_struct("!BBH") + hit = parser.get_bytes(lh) + key = parser.get_bytes(lk) + servers = [] + while parser.remaining() > 0: + server = parser.get_name(origin) + servers.append(server) + return cls(rdclass, rdtype, hit, algorithm, key, servers) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/ISDN.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/ISDN.py new file mode 100644 index 0000000..6428a0a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/ISDN.py @@ -0,0 +1,78 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.rdata +import dns.tokenizer + + +@dns.immutable.immutable +class ISDN(dns.rdata.Rdata): + """ISDN record""" + + # see: RFC 1183 + + __slots__ = ["address", "subaddress"] + + def __init__(self, rdclass, rdtype, address, subaddress): + super().__init__(rdclass, rdtype) + self.address = self._as_bytes(address, True, 255) + self.subaddress = self._as_bytes(subaddress, True, 255) + + def to_text(self, origin=None, relativize=True, **kw): + if self.subaddress: + return ( + f'"{dns.rdata._escapify(self.address)}" ' + f'"{dns.rdata._escapify(self.subaddress)}"' + ) + else: + return f'"{dns.rdata._escapify(self.address)}"' + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + address = tok.get_string() + tokens = tok.get_remaining(max_tokens=1) + if len(tokens) >= 1: + subaddress = tokens[0].unescape().value + else: + subaddress = "" + return cls(rdclass, rdtype, address, subaddress) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + l = len(self.address) + assert l < 256 + file.write(struct.pack("!B", l)) + file.write(self.address) + l = len(self.subaddress) + if l > 0: + assert l < 256 + file.write(struct.pack("!B", l)) + file.write(self.subaddress) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + address = parser.get_counted_bytes() + if parser.remaining() > 0: + subaddress = parser.get_counted_bytes() + else: + subaddress = b"" + return cls(rdclass, rdtype, address, subaddress) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/L32.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/L32.py new file mode 100644 index 0000000..f51e5c7 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/L32.py @@ -0,0 +1,42 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.immutable +import dns.ipv4 +import dns.rdata + + +@dns.immutable.immutable +class L32(dns.rdata.Rdata): + """L32 record""" + + # see: rfc6742.txt + + __slots__ = ["preference", "locator32"] + + def __init__(self, rdclass, rdtype, preference, locator32): + super().__init__(rdclass, rdtype) + self.preference = self._as_uint16(preference) + self.locator32 = self._as_ipv4_address(locator32) + + def to_text(self, origin=None, relativize=True, **kw): + return f"{self.preference} {self.locator32}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + preference = tok.get_uint16() + nodeid = tok.get_identifier() + return cls(rdclass, rdtype, preference, nodeid) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(struct.pack("!H", self.preference)) + file.write(dns.ipv4.inet_aton(self.locator32)) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + preference = parser.get_uint16() + locator32 = parser.get_remaining() + return cls(rdclass, rdtype, preference, locator32) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/L64.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/L64.py new file mode 100644 index 0000000..a47da19 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/L64.py @@ -0,0 +1,48 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.immutable +import dns.rdata +import dns.rdtypes.util + + +@dns.immutable.immutable +class L64(dns.rdata.Rdata): + """L64 record""" + + # see: rfc6742.txt + + __slots__ = ["preference", "locator64"] + + def __init__(self, rdclass, rdtype, preference, locator64): + super().__init__(rdclass, rdtype) + self.preference = self._as_uint16(preference) + if isinstance(locator64, bytes): + if len(locator64) != 8: + raise ValueError("invalid locator64") + self.locator64 = dns.rdata._hexify(locator64, 4, b":") + else: + dns.rdtypes.util.parse_formatted_hex(locator64, 4, 4, ":") + self.locator64 = locator64 + + def to_text(self, origin=None, relativize=True, **kw): + return f"{self.preference} {self.locator64}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + preference = tok.get_uint16() + locator64 = tok.get_identifier() + return cls(rdclass, rdtype, preference, locator64) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(struct.pack("!H", self.preference)) + file.write(dns.rdtypes.util.parse_formatted_hex(self.locator64, 4, 4, ":")) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + preference = parser.get_uint16() + locator64 = parser.get_remaining() + return cls(rdclass, rdtype, preference, locator64) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/LOC.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/LOC.py new file mode 100644 index 0000000..6c7fe5e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/LOC.py @@ -0,0 +1,347 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.rdata + +_pows = tuple(10**i for i in range(0, 11)) + +# default values are in centimeters +_default_size = 100.0 +_default_hprec = 1000000.0 +_default_vprec = 1000.0 + +# for use by from_wire() +_MAX_LATITUDE = 0x80000000 + 90 * 3600000 +_MIN_LATITUDE = 0x80000000 - 90 * 3600000 +_MAX_LONGITUDE = 0x80000000 + 180 * 3600000 +_MIN_LONGITUDE = 0x80000000 - 180 * 3600000 + + +def _exponent_of(what, desc): + if what == 0: + return 0 + exp = None + for i, pow in enumerate(_pows): + if what < pow: + exp = i - 1 + break + if exp is None or exp < 0: + raise dns.exception.SyntaxError(f"{desc} value out of bounds") + return exp + + +def _float_to_tuple(what): + if what < 0: + sign = -1 + what *= -1 + else: + sign = 1 + what = round(what * 3600000) + degrees = int(what // 3600000) + what -= degrees * 3600000 + minutes = int(what // 60000) + what -= minutes * 60000 + seconds = int(what // 1000) + what -= int(seconds * 1000) + what = int(what) + return (degrees, minutes, seconds, what, sign) + + +def _tuple_to_float(what): + value = float(what[0]) + value += float(what[1]) / 60.0 + value += float(what[2]) / 3600.0 + value += float(what[3]) / 3600000.0 + return float(what[4]) * value + + +def _encode_size(what, desc): + what = int(what) + exponent = _exponent_of(what, desc) & 0xF + base = what // pow(10, exponent) & 0xF + return base * 16 + exponent + + +def _decode_size(what, desc): + exponent = what & 0x0F + if exponent > 9: + raise dns.exception.FormError(f"bad {desc} exponent") + base = (what & 0xF0) >> 4 + if base > 9: + raise dns.exception.FormError(f"bad {desc} base") + return base * pow(10, exponent) + + +def _check_coordinate_list(value, low, high): + if value[0] < low or value[0] > high: + raise ValueError(f"not in range [{low}, {high}]") + if value[1] < 0 or value[1] > 59: + raise ValueError("bad minutes value") + if value[2] < 0 or value[2] > 59: + raise ValueError("bad seconds value") + if value[3] < 0 or value[3] > 999: + raise ValueError("bad milliseconds value") + if value[4] != 1 and value[4] != -1: + raise ValueError("bad hemisphere value") + + +@dns.immutable.immutable +class LOC(dns.rdata.Rdata): + """LOC record""" + + # see: RFC 1876 + + __slots__ = [ + "latitude", + "longitude", + "altitude", + "size", + "horizontal_precision", + "vertical_precision", + ] + + def __init__( + self, + rdclass, + rdtype, + latitude, + longitude, + altitude, + size=_default_size, + hprec=_default_hprec, + vprec=_default_vprec, + ): + """Initialize a LOC record instance. + + The parameters I{latitude} and I{longitude} may be either a 4-tuple + of integers specifying (degrees, minutes, seconds, milliseconds), + or they may be floating point values specifying the number of + degrees. The other parameters are floats. Size, horizontal precision, + and vertical precision are specified in centimeters.""" + + super().__init__(rdclass, rdtype) + if isinstance(latitude, int): + latitude = float(latitude) + if isinstance(latitude, float): + latitude = _float_to_tuple(latitude) + _check_coordinate_list(latitude, -90, 90) + self.latitude = tuple(latitude) # pyright: ignore + if isinstance(longitude, int): + longitude = float(longitude) + if isinstance(longitude, float): + longitude = _float_to_tuple(longitude) + _check_coordinate_list(longitude, -180, 180) + self.longitude = tuple(longitude) # pyright: ignore + self.altitude = float(altitude) + self.size = float(size) + self.horizontal_precision = float(hprec) + self.vertical_precision = float(vprec) + + def to_text(self, origin=None, relativize=True, **kw): + if self.latitude[4] > 0: + lat_hemisphere = "N" + else: + lat_hemisphere = "S" + if self.longitude[4] > 0: + long_hemisphere = "E" + else: + long_hemisphere = "W" + text = ( + f"{self.latitude[0]} {self.latitude[1]} " + f"{self.latitude[2]}.{self.latitude[3]:03d} {lat_hemisphere} " + f"{self.longitude[0]} {self.longitude[1]} " + f"{self.longitude[2]}.{self.longitude[3]:03d} {long_hemisphere} " + f"{(self.altitude / 100.0):0.2f}m" + ) + + # do not print default values + if ( + self.size != _default_size + or self.horizontal_precision != _default_hprec + or self.vertical_precision != _default_vprec + ): + text += ( + f" {self.size / 100.0:0.2f}m {self.horizontal_precision / 100.0:0.2f}m" + f" {self.vertical_precision / 100.0:0.2f}m" + ) + return text + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + latitude = [0, 0, 0, 0, 1] + longitude = [0, 0, 0, 0, 1] + size = _default_size + hprec = _default_hprec + vprec = _default_vprec + + latitude[0] = tok.get_int() + t = tok.get_string() + if t.isdigit(): + latitude[1] = int(t) + t = tok.get_string() + if "." in t: + (seconds, milliseconds) = t.split(".") + if not seconds.isdigit(): + raise dns.exception.SyntaxError("bad latitude seconds value") + latitude[2] = int(seconds) + l = len(milliseconds) + if l == 0 or l > 3 or not milliseconds.isdigit(): + raise dns.exception.SyntaxError("bad latitude milliseconds value") + if l == 1: + m = 100 + elif l == 2: + m = 10 + else: + m = 1 + latitude[3] = m * int(milliseconds) + t = tok.get_string() + elif t.isdigit(): + latitude[2] = int(t) + t = tok.get_string() + if t == "S": + latitude[4] = -1 + elif t != "N": + raise dns.exception.SyntaxError("bad latitude hemisphere value") + + longitude[0] = tok.get_int() + t = tok.get_string() + if t.isdigit(): + longitude[1] = int(t) + t = tok.get_string() + if "." in t: + (seconds, milliseconds) = t.split(".") + if not seconds.isdigit(): + raise dns.exception.SyntaxError("bad longitude seconds value") + longitude[2] = int(seconds) + l = len(milliseconds) + if l == 0 or l > 3 or not milliseconds.isdigit(): + raise dns.exception.SyntaxError("bad longitude milliseconds value") + if l == 1: + m = 100 + elif l == 2: + m = 10 + else: + m = 1 + longitude[3] = m * int(milliseconds) + t = tok.get_string() + elif t.isdigit(): + longitude[2] = int(t) + t = tok.get_string() + if t == "W": + longitude[4] = -1 + elif t != "E": + raise dns.exception.SyntaxError("bad longitude hemisphere value") + + t = tok.get_string() + if t[-1] == "m": + t = t[0:-1] + altitude = float(t) * 100.0 # m -> cm + + tokens = tok.get_remaining(max_tokens=3) + if len(tokens) >= 1: + value = tokens[0].unescape().value + if value[-1] == "m": + value = value[0:-1] + size = float(value) * 100.0 # m -> cm + if len(tokens) >= 2: + value = tokens[1].unescape().value + if value[-1] == "m": + value = value[0:-1] + hprec = float(value) * 100.0 # m -> cm + if len(tokens) >= 3: + value = tokens[2].unescape().value + if value[-1] == "m": + value = value[0:-1] + vprec = float(value) * 100.0 # m -> cm + + # Try encoding these now so we raise if they are bad + _encode_size(size, "size") + _encode_size(hprec, "horizontal precision") + _encode_size(vprec, "vertical precision") + + return cls(rdclass, rdtype, latitude, longitude, altitude, size, hprec, vprec) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + milliseconds = ( + self.latitude[0] * 3600000 + + self.latitude[1] * 60000 + + self.latitude[2] * 1000 + + self.latitude[3] + ) * self.latitude[4] + latitude = 0x80000000 + milliseconds + milliseconds = ( + self.longitude[0] * 3600000 + + self.longitude[1] * 60000 + + self.longitude[2] * 1000 + + self.longitude[3] + ) * self.longitude[4] + longitude = 0x80000000 + milliseconds + altitude = int(self.altitude) + 10000000 + size = _encode_size(self.size, "size") + hprec = _encode_size(self.horizontal_precision, "horizontal precision") + vprec = _encode_size(self.vertical_precision, "vertical precision") + wire = struct.pack( + "!BBBBIII", 0, size, hprec, vprec, latitude, longitude, altitude + ) + file.write(wire) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + ( + version, + size, + hprec, + vprec, + latitude, + longitude, + altitude, + ) = parser.get_struct("!BBBBIII") + if version != 0: + raise dns.exception.FormError("LOC version not zero") + if latitude < _MIN_LATITUDE or latitude > _MAX_LATITUDE: + raise dns.exception.FormError("bad latitude") + if latitude > 0x80000000: + latitude = (latitude - 0x80000000) / 3600000 + else: + latitude = -1 * (0x80000000 - latitude) / 3600000 + if longitude < _MIN_LONGITUDE or longitude > _MAX_LONGITUDE: + raise dns.exception.FormError("bad longitude") + if longitude > 0x80000000: + longitude = (longitude - 0x80000000) / 3600000 + else: + longitude = -1 * (0x80000000 - longitude) / 3600000 + altitude = float(altitude) - 10000000.0 + size = _decode_size(size, "size") + hprec = _decode_size(hprec, "horizontal precision") + vprec = _decode_size(vprec, "vertical precision") + return cls(rdclass, rdtype, latitude, longitude, altitude, size, hprec, vprec) + + @property + def float_latitude(self): + "latitude as a floating point value" + return _tuple_to_float(self.latitude) + + @property + def float_longitude(self): + "longitude as a floating point value" + return _tuple_to_float(self.longitude) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/LP.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/LP.py new file mode 100644 index 0000000..379c862 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/LP.py @@ -0,0 +1,42 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.immutable +import dns.rdata + + +@dns.immutable.immutable +class LP(dns.rdata.Rdata): + """LP record""" + + # see: rfc6742.txt + + __slots__ = ["preference", "fqdn"] + + def __init__(self, rdclass, rdtype, preference, fqdn): + super().__init__(rdclass, rdtype) + self.preference = self._as_uint16(preference) + self.fqdn = self._as_name(fqdn) + + def to_text(self, origin=None, relativize=True, **kw): + fqdn = self.fqdn.choose_relativity(origin, relativize) + return f"{self.preference} {fqdn}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + preference = tok.get_uint16() + fqdn = tok.get_name(origin, relativize, relativize_to) + return cls(rdclass, rdtype, preference, fqdn) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(struct.pack("!H", self.preference)) + self.fqdn.to_wire(file, compress, origin, canonicalize) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + preference = parser.get_uint16() + fqdn = parser.get_name(origin) + return cls(rdclass, rdtype, preference, fqdn) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/MX.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/MX.py new file mode 100644 index 0000000..0c300c5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/MX.py @@ -0,0 +1,24 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.mxbase + + +@dns.immutable.immutable +class MX(dns.rdtypes.mxbase.MXBase): + """MX record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NID.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NID.py new file mode 100644 index 0000000..fa0dad5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NID.py @@ -0,0 +1,48 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import struct + +import dns.immutable +import dns.rdata +import dns.rdtypes.util + + +@dns.immutable.immutable +class NID(dns.rdata.Rdata): + """NID record""" + + # see: rfc6742.txt + + __slots__ = ["preference", "nodeid"] + + def __init__(self, rdclass, rdtype, preference, nodeid): + super().__init__(rdclass, rdtype) + self.preference = self._as_uint16(preference) + if isinstance(nodeid, bytes): + if len(nodeid) != 8: + raise ValueError("invalid nodeid") + self.nodeid = dns.rdata._hexify(nodeid, 4, b":") + else: + dns.rdtypes.util.parse_formatted_hex(nodeid, 4, 4, ":") + self.nodeid = nodeid + + def to_text(self, origin=None, relativize=True, **kw): + return f"{self.preference} {self.nodeid}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + preference = tok.get_uint16() + nodeid = tok.get_identifier() + return cls(rdclass, rdtype, preference, nodeid) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(struct.pack("!H", self.preference)) + file.write(dns.rdtypes.util.parse_formatted_hex(self.nodeid, 4, 4, ":")) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + preference = parser.get_uint16() + nodeid = parser.get_remaining() + return cls(rdclass, rdtype, preference, nodeid) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NINFO.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NINFO.py new file mode 100644 index 0000000..b177bdd --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NINFO.py @@ -0,0 +1,26 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.txtbase + + +@dns.immutable.immutable +class NINFO(dns.rdtypes.txtbase.TXTBase): + """NINFO record""" + + # see: draft-reid-dnsext-zs-01 diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NS.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NS.py new file mode 100644 index 0000000..c3f34ce --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NS.py @@ -0,0 +1,24 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.nsbase + + +@dns.immutable.immutable +class NS(dns.rdtypes.nsbase.NSBase): + """NS record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC.py new file mode 100644 index 0000000..3c78b72 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC.py @@ -0,0 +1,67 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata +import dns.rdatatype +import dns.rdtypes.util + + +@dns.immutable.immutable +class Bitmap(dns.rdtypes.util.Bitmap): + type_name = "NSEC" + + +@dns.immutable.immutable +class NSEC(dns.rdata.Rdata): + """NSEC record""" + + __slots__ = ["next", "windows"] + + def __init__(self, rdclass, rdtype, next, windows): + super().__init__(rdclass, rdtype) + self.next = self._as_name(next) + if not isinstance(windows, Bitmap): + windows = Bitmap(windows) + self.windows = tuple(windows.windows) + + def to_text(self, origin=None, relativize=True, **kw): + next = self.next.choose_relativity(origin, relativize) + text = Bitmap(self.windows).to_text() + return f"{next}{text}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + next = tok.get_name(origin, relativize, relativize_to) + windows = Bitmap.from_text(tok) + return cls(rdclass, rdtype, next, windows) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + # Note that NSEC downcasing, originally mandated by RFC 4034 + # section 6.2 was removed by RFC 6840 section 5.1. + self.next.to_wire(file, None, origin, False) + Bitmap(self.windows).to_wire(file) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + next = parser.get_name(origin) + bitmap = Bitmap.from_wire_parser(parser) + return cls(rdclass, rdtype, next, bitmap) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3.py new file mode 100644 index 0000000..6899418 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3.py @@ -0,0 +1,120 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 +import binascii +import struct + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata +import dns.rdatatype +import dns.rdtypes.util + +b32_hex_to_normal = bytes.maketrans( + b"0123456789ABCDEFGHIJKLMNOPQRSTUV", b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567" +) +b32_normal_to_hex = bytes.maketrans( + b"ABCDEFGHIJKLMNOPQRSTUVWXYZ234567", b"0123456789ABCDEFGHIJKLMNOPQRSTUV" +) + +# hash algorithm constants +SHA1 = 1 + +# flag constants +OPTOUT = 1 + + +@dns.immutable.immutable +class Bitmap(dns.rdtypes.util.Bitmap): + type_name = "NSEC3" + + +@dns.immutable.immutable +class NSEC3(dns.rdata.Rdata): + """NSEC3 record""" + + __slots__ = ["algorithm", "flags", "iterations", "salt", "next", "windows"] + + def __init__( + self, rdclass, rdtype, algorithm, flags, iterations, salt, next, windows + ): + super().__init__(rdclass, rdtype) + self.algorithm = self._as_uint8(algorithm) + self.flags = self._as_uint8(flags) + self.iterations = self._as_uint16(iterations) + self.salt = self._as_bytes(salt, True, 255) + self.next = self._as_bytes(next, True, 255) + if not isinstance(windows, Bitmap): + windows = Bitmap(windows) + self.windows = tuple(windows.windows) + + def _next_text(self): + next = base64.b32encode(self.next).translate(b32_normal_to_hex).lower().decode() + next = next.rstrip("=") + return next + + def to_text(self, origin=None, relativize=True, **kw): + next = self._next_text() + if self.salt == b"": + salt = "-" + else: + salt = binascii.hexlify(self.salt).decode() + text = Bitmap(self.windows).to_text() + return f"{self.algorithm} {self.flags} {self.iterations} {salt} {next}{text}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + algorithm = tok.get_uint8() + flags = tok.get_uint8() + iterations = tok.get_uint16() + salt = tok.get_string() + if salt == "-": + salt = b"" + else: + salt = binascii.unhexlify(salt.encode("ascii")) + next = tok.get_string().encode("ascii").upper().translate(b32_hex_to_normal) + if next.endswith(b"="): + raise binascii.Error("Incorrect padding") + if len(next) % 8 != 0: + next += b"=" * (8 - len(next) % 8) + next = base64.b32decode(next) + bitmap = Bitmap.from_text(tok) + return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next, bitmap) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + l = len(self.salt) + file.write(struct.pack("!BBHB", self.algorithm, self.flags, self.iterations, l)) + file.write(self.salt) + l = len(self.next) + file.write(struct.pack("!B", l)) + file.write(self.next) + Bitmap(self.windows).to_wire(file) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (algorithm, flags, iterations) = parser.get_struct("!BBH") + salt = parser.get_counted_bytes() + next = parser.get_counted_bytes() + bitmap = Bitmap.from_wire_parser(parser) + return cls(rdclass, rdtype, algorithm, flags, iterations, salt, next, bitmap) + + def next_name(self, origin=None): + return dns.name.from_text(self._next_text(), origin) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py new file mode 100644 index 0000000..e867872 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py @@ -0,0 +1,69 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import binascii +import struct + +import dns.exception +import dns.immutable +import dns.rdata + + +@dns.immutable.immutable +class NSEC3PARAM(dns.rdata.Rdata): + """NSEC3PARAM record""" + + __slots__ = ["algorithm", "flags", "iterations", "salt"] + + def __init__(self, rdclass, rdtype, algorithm, flags, iterations, salt): + super().__init__(rdclass, rdtype) + self.algorithm = self._as_uint8(algorithm) + self.flags = self._as_uint8(flags) + self.iterations = self._as_uint16(iterations) + self.salt = self._as_bytes(salt, True, 255) + + def to_text(self, origin=None, relativize=True, **kw): + if self.salt == b"": + salt = "-" + else: + salt = binascii.hexlify(self.salt).decode() + return f"{self.algorithm} {self.flags} {self.iterations} {salt}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + algorithm = tok.get_uint8() + flags = tok.get_uint8() + iterations = tok.get_uint16() + salt = tok.get_string() + if salt == "-": + salt = "" + else: + salt = binascii.unhexlify(salt.encode()) + return cls(rdclass, rdtype, algorithm, flags, iterations, salt) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + l = len(self.salt) + file.write(struct.pack("!BBHB", self.algorithm, self.flags, self.iterations, l)) + file.write(self.salt) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (algorithm, flags, iterations) = parser.get_struct("!BBH") + salt = parser.get_counted_bytes() + return cls(rdclass, rdtype, algorithm, flags, iterations, salt) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py new file mode 100644 index 0000000..ac1841c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py @@ -0,0 +1,53 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2016 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 + +import dns.exception +import dns.immutable +import dns.rdata +import dns.tokenizer + + +@dns.immutable.immutable +class OPENPGPKEY(dns.rdata.Rdata): + """OPENPGPKEY record""" + + # see: RFC 7929 + + def __init__(self, rdclass, rdtype, key): + super().__init__(rdclass, rdtype) + self.key = self._as_bytes(key) + + def to_text(self, origin=None, relativize=True, **kw): + return dns.rdata._base64ify(self.key, chunksize=None, **kw) # pyright: ignore + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + b64 = tok.concatenate_remaining_identifiers().encode() + key = base64.b64decode(b64) + return cls(rdclass, rdtype, key) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(self.key) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + key = parser.get_remaining() + return cls(rdclass, rdtype, key) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/OPT.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/OPT.py new file mode 100644 index 0000000..d343dfa --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/OPT.py @@ -0,0 +1,77 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.edns +import dns.exception +import dns.immutable +import dns.rdata + +# We don't implement from_text, and that's ok. +# pylint: disable=abstract-method + + +@dns.immutable.immutable +class OPT(dns.rdata.Rdata): + """OPT record""" + + __slots__ = ["options"] + + def __init__(self, rdclass, rdtype, options): + """Initialize an OPT rdata. + + *rdclass*, an ``int`` is the rdataclass of the Rdata, + which is also the payload size. + + *rdtype*, an ``int`` is the rdatatype of the Rdata. + + *options*, a tuple of ``bytes`` + """ + + super().__init__(rdclass, rdtype) + + def as_option(option): + if not isinstance(option, dns.edns.Option): + raise ValueError("option is not a dns.edns.option") + return option + + self.options = self._as_tuple(options, as_option) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + for opt in self.options: + owire = opt.to_wire() + file.write(struct.pack("!HH", opt.otype, len(owire))) + file.write(owire) + + def to_text(self, origin=None, relativize=True, **kw): + return " ".join(opt.to_text() for opt in self.options) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + options = [] + while parser.remaining() > 0: + (otype, olen) = parser.get_struct("!HH") + with parser.restrict_to(olen): + opt = dns.edns.option_from_wire_parser(otype, parser) + options.append(opt) + return cls(rdclass, rdtype, options) + + @property + def payload(self): + "payload size" + return self.rdclass diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/PTR.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/PTR.py new file mode 100644 index 0000000..98c3616 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/PTR.py @@ -0,0 +1,24 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.nsbase + + +@dns.immutable.immutable +class PTR(dns.rdtypes.nsbase.NSBase): + """PTR record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RESINFO.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RESINFO.py new file mode 100644 index 0000000..76c8ea2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RESINFO.py @@ -0,0 +1,24 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.txtbase + + +@dns.immutable.immutable +class RESINFO(dns.rdtypes.txtbase.TXTBase): + """RESINFO record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RP.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RP.py new file mode 100644 index 0000000..a66cfc5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RP.py @@ -0,0 +1,58 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata + + +@dns.immutable.immutable +class RP(dns.rdata.Rdata): + """RP record""" + + # see: RFC 1183 + + __slots__ = ["mbox", "txt"] + + def __init__(self, rdclass, rdtype, mbox, txt): + super().__init__(rdclass, rdtype) + self.mbox = self._as_name(mbox) + self.txt = self._as_name(txt) + + def to_text(self, origin=None, relativize=True, **kw): + mbox = self.mbox.choose_relativity(origin, relativize) + txt = self.txt.choose_relativity(origin, relativize) + return f"{str(mbox)} {str(txt)}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + mbox = tok.get_name(origin, relativize, relativize_to) + txt = tok.get_name(origin, relativize, relativize_to) + return cls(rdclass, rdtype, mbox, txt) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.mbox.to_wire(file, None, origin, canonicalize) + self.txt.to_wire(file, None, origin, canonicalize) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + mbox = parser.get_name(origin) + txt = parser.get_name(origin) + return cls(rdclass, rdtype, mbox, txt) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RRSIG.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RRSIG.py new file mode 100644 index 0000000..5556cba --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RRSIG.py @@ -0,0 +1,155 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 +import calendar +import struct +import time + +import dns.dnssectypes +import dns.exception +import dns.immutable +import dns.rdata +import dns.rdatatype + + +class BadSigTime(dns.exception.DNSException): + """Time in DNS SIG or RRSIG resource record cannot be parsed.""" + + +def sigtime_to_posixtime(what): + if len(what) <= 10 and what.isdigit(): + return int(what) + if len(what) != 14: + raise BadSigTime + year = int(what[0:4]) + month = int(what[4:6]) + day = int(what[6:8]) + hour = int(what[8:10]) + minute = int(what[10:12]) + second = int(what[12:14]) + return calendar.timegm((year, month, day, hour, minute, second, 0, 0, 0)) + + +def posixtime_to_sigtime(what): + return time.strftime("%Y%m%d%H%M%S", time.gmtime(what)) + + +@dns.immutable.immutable +class RRSIG(dns.rdata.Rdata): + """RRSIG record""" + + __slots__ = [ + "type_covered", + "algorithm", + "labels", + "original_ttl", + "expiration", + "inception", + "key_tag", + "signer", + "signature", + ] + + def __init__( + self, + rdclass, + rdtype, + type_covered, + algorithm, + labels, + original_ttl, + expiration, + inception, + key_tag, + signer, + signature, + ): + super().__init__(rdclass, rdtype) + self.type_covered = self._as_rdatatype(type_covered) + self.algorithm = dns.dnssectypes.Algorithm.make(algorithm) + self.labels = self._as_uint8(labels) + self.original_ttl = self._as_ttl(original_ttl) + self.expiration = self._as_uint32(expiration) + self.inception = self._as_uint32(inception) + self.key_tag = self._as_uint16(key_tag) + self.signer = self._as_name(signer) + self.signature = self._as_bytes(signature) + + def covers(self): + return self.type_covered + + def to_text(self, origin=None, relativize=True, **kw): + return ( + f"{dns.rdatatype.to_text(self.type_covered)} " + f"{self.algorithm} {self.labels} {self.original_ttl} " + f"{posixtime_to_sigtime(self.expiration)} " + f"{posixtime_to_sigtime(self.inception)} " + f"{self.key_tag} " + f"{self.signer.choose_relativity(origin, relativize)} " + f"{dns.rdata._base64ify(self.signature, **kw)}" # pyright: ignore + ) + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + type_covered = dns.rdatatype.from_text(tok.get_string()) + algorithm = dns.dnssectypes.Algorithm.from_text(tok.get_string()) + labels = tok.get_int() + original_ttl = tok.get_ttl() + expiration = sigtime_to_posixtime(tok.get_string()) + inception = sigtime_to_posixtime(tok.get_string()) + key_tag = tok.get_int() + signer = tok.get_name(origin, relativize, relativize_to) + b64 = tok.concatenate_remaining_identifiers().encode() + signature = base64.b64decode(b64) + return cls( + rdclass, + rdtype, + type_covered, + algorithm, + labels, + original_ttl, + expiration, + inception, + key_tag, + signer, + signature, + ) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + header = struct.pack( + "!HBBIIIH", + self.type_covered, + self.algorithm, + self.labels, + self.original_ttl, + self.expiration, + self.inception, + self.key_tag, + ) + file.write(header) + self.signer.to_wire(file, None, origin, canonicalize) + file.write(self.signature) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct("!HBBIIIH") + signer = parser.get_name(origin) + signature = parser.get_remaining() + return cls(rdclass, rdtype, *header, signer, signature) # pyright: ignore diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RT.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RT.py new file mode 100644 index 0000000..5a4d45c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/RT.py @@ -0,0 +1,24 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.mxbase + + +@dns.immutable.immutable +class RT(dns.rdtypes.mxbase.UncompressedDowncasingMX): + """RT record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SMIMEA.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SMIMEA.py new file mode 100644 index 0000000..55d87bf --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SMIMEA.py @@ -0,0 +1,9 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import dns.immutable +import dns.rdtypes.tlsabase + + +@dns.immutable.immutable +class SMIMEA(dns.rdtypes.tlsabase.TLSABase): + """SMIMEA record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SOA.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SOA.py new file mode 100644 index 0000000..3c7cd8c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SOA.py @@ -0,0 +1,78 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata + + +@dns.immutable.immutable +class SOA(dns.rdata.Rdata): + """SOA record""" + + # see: RFC 1035 + + __slots__ = ["mname", "rname", "serial", "refresh", "retry", "expire", "minimum"] + + def __init__( + self, rdclass, rdtype, mname, rname, serial, refresh, retry, expire, minimum + ): + super().__init__(rdclass, rdtype) + self.mname = self._as_name(mname) + self.rname = self._as_name(rname) + self.serial = self._as_uint32(serial) + self.refresh = self._as_ttl(refresh) + self.retry = self._as_ttl(retry) + self.expire = self._as_ttl(expire) + self.minimum = self._as_ttl(minimum) + + def to_text(self, origin=None, relativize=True, **kw): + mname = self.mname.choose_relativity(origin, relativize) + rname = self.rname.choose_relativity(origin, relativize) + return f"{mname} {rname} {self.serial} {self.refresh} {self.retry} {self.expire} {self.minimum}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + mname = tok.get_name(origin, relativize, relativize_to) + rname = tok.get_name(origin, relativize, relativize_to) + serial = tok.get_uint32() + refresh = tok.get_ttl() + retry = tok.get_ttl() + expire = tok.get_ttl() + minimum = tok.get_ttl() + return cls( + rdclass, rdtype, mname, rname, serial, refresh, retry, expire, minimum + ) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.mname.to_wire(file, compress, origin, canonicalize) + self.rname.to_wire(file, compress, origin, canonicalize) + five_ints = struct.pack( + "!IIIII", self.serial, self.refresh, self.retry, self.expire, self.minimum + ) + file.write(five_ints) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + mname = parser.get_name(origin) + rname = parser.get_name(origin) + return cls(rdclass, rdtype, mname, rname, *parser.get_struct("!IIIII")) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SPF.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SPF.py new file mode 100644 index 0000000..1df3b70 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SPF.py @@ -0,0 +1,26 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.txtbase + + +@dns.immutable.immutable +class SPF(dns.rdtypes.txtbase.TXTBase): + """SPF record""" + + # see: RFC 4408 diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SSHFP.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SSHFP.py new file mode 100644 index 0000000..3f08f3a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/SSHFP.py @@ -0,0 +1,67 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2005-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import binascii +import struct + +import dns.immutable +import dns.rdata +import dns.rdatatype + + +@dns.immutable.immutable +class SSHFP(dns.rdata.Rdata): + """SSHFP record""" + + # See RFC 4255 + + __slots__ = ["algorithm", "fp_type", "fingerprint"] + + def __init__(self, rdclass, rdtype, algorithm, fp_type, fingerprint): + super().__init__(rdclass, rdtype) + self.algorithm = self._as_uint8(algorithm) + self.fp_type = self._as_uint8(fp_type) + self.fingerprint = self._as_bytes(fingerprint, True) + + def to_text(self, origin=None, relativize=True, **kw): + kw = kw.copy() + chunksize = kw.pop("chunksize", 128) + fingerprint = dns.rdata._hexify( + self.fingerprint, chunksize=chunksize, **kw # pyright: ignore + ) + return f"{self.algorithm} {self.fp_type} {fingerprint}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + algorithm = tok.get_uint8() + fp_type = tok.get_uint8() + fingerprint = tok.concatenate_remaining_identifiers().encode() + fingerprint = binascii.unhexlify(fingerprint) + return cls(rdclass, rdtype, algorithm, fp_type, fingerprint) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + header = struct.pack("!BB", self.algorithm, self.fp_type) + file.write(header) + file.write(self.fingerprint) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct("BB") + fingerprint = parser.get_remaining() + return cls(rdclass, rdtype, header[0], header[1], fingerprint) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TKEY.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TKEY.py new file mode 100644 index 0000000..f9189b1 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TKEY.py @@ -0,0 +1,135 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 +import struct + +import dns.exception +import dns.immutable +import dns.rdata + + +@dns.immutable.immutable +class TKEY(dns.rdata.Rdata): + """TKEY Record""" + + __slots__ = [ + "algorithm", + "inception", + "expiration", + "mode", + "error", + "key", + "other", + ] + + def __init__( + self, + rdclass, + rdtype, + algorithm, + inception, + expiration, + mode, + error, + key, + other=b"", + ): + super().__init__(rdclass, rdtype) + self.algorithm = self._as_name(algorithm) + self.inception = self._as_uint32(inception) + self.expiration = self._as_uint32(expiration) + self.mode = self._as_uint16(mode) + self.error = self._as_uint16(error) + self.key = self._as_bytes(key) + self.other = self._as_bytes(other) + + def to_text(self, origin=None, relativize=True, **kw): + _algorithm = self.algorithm.choose_relativity(origin, relativize) + key = dns.rdata._base64ify(self.key, 0) + other = "" + if len(self.other) > 0: + other = " " + dns.rdata._base64ify(self.other, 0) + return f"{_algorithm} {self.inception} {self.expiration} {self.mode} {self.error} {key}{other}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + algorithm = tok.get_name(relativize=False) + inception = tok.get_uint32() + expiration = tok.get_uint32() + mode = tok.get_uint16() + error = tok.get_uint16() + key_b64 = tok.get_string().encode() + key = base64.b64decode(key_b64) + other_b64 = tok.concatenate_remaining_identifiers(True).encode() + other = base64.b64decode(other_b64) + + return cls( + rdclass, rdtype, algorithm, inception, expiration, mode, error, key, other + ) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.algorithm.to_wire(file, compress, origin) + file.write( + struct.pack("!IIHH", self.inception, self.expiration, self.mode, self.error) + ) + file.write(struct.pack("!H", len(self.key))) + file.write(self.key) + file.write(struct.pack("!H", len(self.other))) + if len(self.other) > 0: + file.write(self.other) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + algorithm = parser.get_name(origin) + inception, expiration, mode, error = parser.get_struct("!IIHH") + key = parser.get_counted_bytes(2) + other = parser.get_counted_bytes(2) + + return cls( + rdclass, rdtype, algorithm, inception, expiration, mode, error, key, other + ) + + # Constants for the mode field - from RFC 2930: + # 2.5 The Mode Field + # + # The mode field specifies the general scheme for key agreement or + # the purpose of the TKEY DNS message. Servers and resolvers + # supporting this specification MUST implement the Diffie-Hellman key + # agreement mode and the key deletion mode for queries. All other + # modes are OPTIONAL. A server supporting TKEY that receives a TKEY + # request with a mode it does not support returns the BADMODE error. + # The following values of the Mode octet are defined, available, or + # reserved: + # + # Value Description + # ----- ----------- + # 0 - reserved, see section 7 + # 1 server assignment + # 2 Diffie-Hellman exchange + # 3 GSS-API negotiation + # 4 resolver assignment + # 5 key deletion + # 6-65534 - available, see section 7 + # 65535 - reserved, see section 7 + SERVER_ASSIGNMENT = 1 + DIFFIE_HELLMAN_EXCHANGE = 2 + GSSAPI_NEGOTIATION = 3 + RESOLVER_ASSIGNMENT = 4 + KEY_DELETION = 5 diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TLSA.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TLSA.py new file mode 100644 index 0000000..4dffc55 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TLSA.py @@ -0,0 +1,9 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import dns.immutable +import dns.rdtypes.tlsabase + + +@dns.immutable.immutable +class TLSA(dns.rdtypes.tlsabase.TLSABase): + """TLSA record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TSIG.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TSIG.py new file mode 100644 index 0000000..7942382 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TSIG.py @@ -0,0 +1,160 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 +import struct + +import dns.exception +import dns.immutable +import dns.rcode +import dns.rdata + + +@dns.immutable.immutable +class TSIG(dns.rdata.Rdata): + """TSIG record""" + + __slots__ = [ + "algorithm", + "time_signed", + "fudge", + "mac", + "original_id", + "error", + "other", + ] + + def __init__( + self, + rdclass, + rdtype, + algorithm, + time_signed, + fudge, + mac, + original_id, + error, + other, + ): + """Initialize a TSIG rdata. + + *rdclass*, an ``int`` is the rdataclass of the Rdata. + + *rdtype*, an ``int`` is the rdatatype of the Rdata. + + *algorithm*, a ``dns.name.Name``. + + *time_signed*, an ``int``. + + *fudge*, an ``int`. + + *mac*, a ``bytes`` + + *original_id*, an ``int`` + + *error*, an ``int`` + + *other*, a ``bytes`` + """ + + super().__init__(rdclass, rdtype) + self.algorithm = self._as_name(algorithm) + self.time_signed = self._as_uint48(time_signed) + self.fudge = self._as_uint16(fudge) + self.mac = self._as_bytes(mac) + self.original_id = self._as_uint16(original_id) + self.error = dns.rcode.Rcode.make(error) + self.other = self._as_bytes(other) + + def to_text(self, origin=None, relativize=True, **kw): + algorithm = self.algorithm.choose_relativity(origin, relativize) + error = dns.rcode.to_text(self.error, True) + text = ( + f"{algorithm} {self.time_signed} {self.fudge} " + + f"{len(self.mac)} {dns.rdata._base64ify(self.mac, 0)} " + + f"{self.original_id} {error} {len(self.other)}" + ) + if self.other: + text += f" {dns.rdata._base64ify(self.other, 0)}" + return text + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + algorithm = tok.get_name(relativize=False) + time_signed = tok.get_uint48() + fudge = tok.get_uint16() + mac_len = tok.get_uint16() + mac = base64.b64decode(tok.get_string()) + if len(mac) != mac_len: + raise SyntaxError("invalid MAC") + original_id = tok.get_uint16() + error = dns.rcode.from_text(tok.get_string()) + other_len = tok.get_uint16() + if other_len > 0: + other = base64.b64decode(tok.get_string()) + if len(other) != other_len: + raise SyntaxError("invalid other data") + else: + other = b"" + return cls( + rdclass, + rdtype, + algorithm, + time_signed, + fudge, + mac, + original_id, + error, + other, + ) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.algorithm.to_wire(file, None, origin, False) + file.write( + struct.pack( + "!HIHH", + (self.time_signed >> 32) & 0xFFFF, + self.time_signed & 0xFFFFFFFF, + self.fudge, + len(self.mac), + ) + ) + file.write(self.mac) + file.write(struct.pack("!HHH", self.original_id, self.error, len(self.other))) + file.write(self.other) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + algorithm = parser.get_name() + time_signed = parser.get_uint48() + fudge = parser.get_uint16() + mac = parser.get_counted_bytes(2) + (original_id, error) = parser.get_struct("!HH") + other = parser.get_counted_bytes(2) + return cls( + rdclass, + rdtype, + algorithm, + time_signed, + fudge, + mac, + original_id, + error, + other, + ) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TXT.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TXT.py new file mode 100644 index 0000000..6d4dae2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/TXT.py @@ -0,0 +1,24 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.txtbase + + +@dns.immutable.immutable +class TXT(dns.rdtypes.txtbase.TXTBase): + """TXT record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/URI.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/URI.py new file mode 100644 index 0000000..021391d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/URI.py @@ -0,0 +1,79 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# Copyright (C) 2015 Red Hat, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata +import dns.rdtypes.util + + +@dns.immutable.immutable +class URI(dns.rdata.Rdata): + """URI record""" + + # see RFC 7553 + + __slots__ = ["priority", "weight", "target"] + + def __init__(self, rdclass, rdtype, priority, weight, target): + super().__init__(rdclass, rdtype) + self.priority = self._as_uint16(priority) + self.weight = self._as_uint16(weight) + self.target = self._as_bytes(target, True) + if len(self.target) == 0: + raise dns.exception.SyntaxError("URI target cannot be empty") + + def to_text(self, origin=None, relativize=True, **kw): + return f'{self.priority} {self.weight} "{self.target.decode()}"' + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + priority = tok.get_uint16() + weight = tok.get_uint16() + target = tok.get().unescape() + if not (target.is_quoted_string() or target.is_identifier()): + raise dns.exception.SyntaxError("URI target must be a string") + return cls(rdclass, rdtype, priority, weight, target.value) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + two_ints = struct.pack("!HH", self.priority, self.weight) + file.write(two_ints) + file.write(self.target) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (priority, weight) = parser.get_struct("!HH") + target = parser.get_remaining() + if len(target) == 0: + raise dns.exception.FormError("URI target may not be empty") + return cls(rdclass, rdtype, priority, weight, target) + + def _processing_priority(self): + return self.priority + + def _processing_weight(self): + return self.weight + + @classmethod + def _processing_order(cls, iterable): + return dns.rdtypes.util.weighted_processing_order(iterable) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/WALLET.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/WALLET.py new file mode 100644 index 0000000..ff46476 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/WALLET.py @@ -0,0 +1,9 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import dns.immutable +import dns.rdtypes.txtbase + + +@dns.immutable.immutable +class WALLET(dns.rdtypes.txtbase.TXTBase): + """WALLET record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/X25.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/X25.py new file mode 100644 index 0000000..2436ddb --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/X25.py @@ -0,0 +1,57 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.rdata +import dns.tokenizer + + +@dns.immutable.immutable +class X25(dns.rdata.Rdata): + """X25 record""" + + # see RFC 1183 + + __slots__ = ["address"] + + def __init__(self, rdclass, rdtype, address): + super().__init__(rdclass, rdtype) + self.address = self._as_bytes(address, True, 255) + + def to_text(self, origin=None, relativize=True, **kw): + return f'"{dns.rdata._escapify(self.address)}"' + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + address = tok.get_string() + return cls(rdclass, rdtype, address) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + l = len(self.address) + assert l < 256 + file.write(struct.pack("!B", l)) + file.write(self.address) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + address = parser.get_counted_bytes() + return cls(rdclass, rdtype, address) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/ZONEMD.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/ZONEMD.py new file mode 100644 index 0000000..acef4f2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/ZONEMD.py @@ -0,0 +1,64 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import binascii +import struct + +import dns.immutable +import dns.rdata +import dns.rdatatype +import dns.zonetypes + + +@dns.immutable.immutable +class ZONEMD(dns.rdata.Rdata): + """ZONEMD record""" + + # See RFC 8976 + + __slots__ = ["serial", "scheme", "hash_algorithm", "digest"] + + def __init__(self, rdclass, rdtype, serial, scheme, hash_algorithm, digest): + super().__init__(rdclass, rdtype) + self.serial = self._as_uint32(serial) + self.scheme = dns.zonetypes.DigestScheme.make(scheme) + self.hash_algorithm = dns.zonetypes.DigestHashAlgorithm.make(hash_algorithm) + self.digest = self._as_bytes(digest) + + if self.scheme == 0: # reserved, RFC 8976 Sec. 5.2 + raise ValueError("scheme 0 is reserved") + if self.hash_algorithm == 0: # reserved, RFC 8976 Sec. 5.3 + raise ValueError("hash_algorithm 0 is reserved") + + hasher = dns.zonetypes._digest_hashers.get(self.hash_algorithm) + if hasher and hasher().digest_size != len(self.digest): + raise ValueError("digest length inconsistent with hash algorithm") + + def to_text(self, origin=None, relativize=True, **kw): + kw = kw.copy() + chunksize = kw.pop("chunksize", 128) + digest = dns.rdata._hexify( + self.digest, chunksize=chunksize, **kw # pyright: ignore + ) + return f"{self.serial} {self.scheme} {self.hash_algorithm} {digest}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + serial = tok.get_uint32() + scheme = tok.get_uint8() + hash_algorithm = tok.get_uint8() + digest = tok.concatenate_remaining_identifiers().encode() + digest = binascii.unhexlify(digest) + return cls(rdclass, rdtype, serial, scheme, hash_algorithm, digest) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + header = struct.pack("!IBB", self.serial, self.scheme, self.hash_algorithm) + file.write(header) + file.write(self.digest) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct("!IBB") + digest = parser.get_remaining() + return cls(rdclass, rdtype, header[0], header[1], header[2], digest) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/__init__.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/__init__.py new file mode 100644 index 0000000..cc39f86 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/ANY/__init__.py @@ -0,0 +1,71 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Class ANY (generic) rdata type classes.""" + +__all__ = [ + "AFSDB", + "AMTRELAY", + "AVC", + "CAA", + "CDNSKEY", + "CDS", + "CERT", + "CNAME", + "CSYNC", + "DLV", + "DNAME", + "DNSKEY", + "DS", + "DSYNC", + "EUI48", + "EUI64", + "GPOS", + "HINFO", + "HIP", + "ISDN", + "L32", + "L64", + "LOC", + "LP", + "MX", + "NID", + "NINFO", + "NS", + "NSEC", + "NSEC3", + "NSEC3PARAM", + "OPENPGPKEY", + "OPT", + "PTR", + "RESINFO", + "RP", + "RRSIG", + "RT", + "SMIMEA", + "SOA", + "SPF", + "SSHFP", + "TKEY", + "TLSA", + "TSIG", + "TXT", + "URI", + "WALLET", + "X25", + "ZONEMD", +] diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/CH/A.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/CH/A.py new file mode 100644 index 0000000..e3e0752 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/CH/A.py @@ -0,0 +1,60 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.immutable +import dns.rdata +import dns.rdtypes.mxbase + + +@dns.immutable.immutable +class A(dns.rdata.Rdata): + """A record for Chaosnet""" + + # domain: the domain of the address + # address: the 16-bit address + + __slots__ = ["domain", "address"] + + def __init__(self, rdclass, rdtype, domain, address): + super().__init__(rdclass, rdtype) + self.domain = self._as_name(domain) + self.address = self._as_uint16(address) + + def to_text(self, origin=None, relativize=True, **kw): + domain = self.domain.choose_relativity(origin, relativize) + return f"{domain} {self.address:o}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + domain = tok.get_name(origin, relativize, relativize_to) + address = tok.get_uint16(base=8) + return cls(rdclass, rdtype, domain, address) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.domain.to_wire(file, compress, origin, canonicalize) + pref = struct.pack("!H", self.address) + file.write(pref) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + domain = parser.get_name(origin) + address = parser.get_uint16() + return cls(rdclass, rdtype, domain, address) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/CH/__init__.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/CH/__init__.py new file mode 100644 index 0000000..0760c26 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/CH/__init__.py @@ -0,0 +1,22 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Class CH rdata type classes.""" + +__all__ = [ + "A", +] diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/A.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/A.py new file mode 100644 index 0000000..e09d611 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/A.py @@ -0,0 +1,51 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.exception +import dns.immutable +import dns.ipv4 +import dns.rdata +import dns.tokenizer + + +@dns.immutable.immutable +class A(dns.rdata.Rdata): + """A record.""" + + __slots__ = ["address"] + + def __init__(self, rdclass, rdtype, address): + super().__init__(rdclass, rdtype) + self.address = self._as_ipv4_address(address) + + def to_text(self, origin=None, relativize=True, **kw): + return self.address + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + address = tok.get_identifier() + return cls(rdclass, rdtype, address) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(dns.ipv4.inet_aton(self.address)) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + address = parser.get_remaining() + return cls(rdclass, rdtype, address) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/AAAA.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/AAAA.py new file mode 100644 index 0000000..0cd139e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/AAAA.py @@ -0,0 +1,51 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.exception +import dns.immutable +import dns.ipv6 +import dns.rdata +import dns.tokenizer + + +@dns.immutable.immutable +class AAAA(dns.rdata.Rdata): + """AAAA record.""" + + __slots__ = ["address"] + + def __init__(self, rdclass, rdtype, address): + super().__init__(rdclass, rdtype) + self.address = self._as_ipv6_address(address) + + def to_text(self, origin=None, relativize=True, **kw): + return self.address + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + address = tok.get_identifier() + return cls(rdclass, rdtype, address) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(dns.ipv6.inet_aton(self.address)) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + address = parser.get_remaining() + return cls(rdclass, rdtype, address) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/APL.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/APL.py new file mode 100644 index 0000000..c4ce6e4 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/APL.py @@ -0,0 +1,150 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import binascii +import codecs +import struct + +import dns.exception +import dns.immutable +import dns.ipv4 +import dns.ipv6 +import dns.rdata +import dns.tokenizer + + +@dns.immutable.immutable +class APLItem: + """An APL list item.""" + + __slots__ = ["family", "negation", "address", "prefix"] + + def __init__(self, family, negation, address, prefix): + self.family = dns.rdata.Rdata._as_uint16(family) + self.negation = dns.rdata.Rdata._as_bool(negation) + if self.family == 1: + self.address = dns.rdata.Rdata._as_ipv4_address(address) + self.prefix = dns.rdata.Rdata._as_int(prefix, 0, 32) + elif self.family == 2: + self.address = dns.rdata.Rdata._as_ipv6_address(address) + self.prefix = dns.rdata.Rdata._as_int(prefix, 0, 128) + else: + self.address = dns.rdata.Rdata._as_bytes(address, max_length=127) + self.prefix = dns.rdata.Rdata._as_uint8(prefix) + + def __str__(self): + if self.negation: + return f"!{self.family}:{self.address}/{self.prefix}" + else: + return f"{self.family}:{self.address}/{self.prefix}" + + def to_wire(self, file): + if self.family == 1: + address = dns.ipv4.inet_aton(self.address) + elif self.family == 2: + address = dns.ipv6.inet_aton(self.address) + else: + address = binascii.unhexlify(self.address) + # + # Truncate least significant zero bytes. + # + last = 0 + for i in range(len(address) - 1, -1, -1): + if address[i] != 0: + last = i + 1 + break + address = address[0:last] + l = len(address) + assert l < 128 + if self.negation: + l |= 0x80 + header = struct.pack("!HBB", self.family, self.prefix, l) + file.write(header) + file.write(address) + + +@dns.immutable.immutable +class APL(dns.rdata.Rdata): + """APL record.""" + + # see: RFC 3123 + + __slots__ = ["items"] + + def __init__(self, rdclass, rdtype, items): + super().__init__(rdclass, rdtype) + for item in items: + if not isinstance(item, APLItem): + raise ValueError("item not an APLItem") + self.items = tuple(items) + + def to_text(self, origin=None, relativize=True, **kw): + return " ".join(map(str, self.items)) + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + items = [] + for token in tok.get_remaining(): + item = token.unescape().value + if item[0] == "!": + negation = True + item = item[1:] + else: + negation = False + (family, rest) = item.split(":", 1) + family = int(family) + (address, prefix) = rest.split("/", 1) + prefix = int(prefix) + item = APLItem(family, negation, address, prefix) + items.append(item) + + return cls(rdclass, rdtype, items) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + for item in self.items: + item.to_wire(file) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + items = [] + while parser.remaining() > 0: + header = parser.get_struct("!HBB") + afdlen = header[2] + if afdlen > 127: + negation = True + afdlen -= 128 + else: + negation = False + address = parser.get_bytes(afdlen) + l = len(address) + if header[0] == 1: + if l < 4: + address += b"\x00" * (4 - l) + elif header[0] == 2: + if l < 16: + address += b"\x00" * (16 - l) + else: + # + # This isn't really right according to the RFC, but it + # seems better than throwing an exception + # + address = codecs.encode(address, "hex_codec") + item = APLItem(header[0], negation, address, header[1]) + items.append(item) + return cls(rdclass, rdtype, items) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/DHCID.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/DHCID.py new file mode 100644 index 0000000..8de8cdf --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/DHCID.py @@ -0,0 +1,54 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 + +import dns.exception +import dns.immutable +import dns.rdata + + +@dns.immutable.immutable +class DHCID(dns.rdata.Rdata): + """DHCID record""" + + # see: RFC 4701 + + __slots__ = ["data"] + + def __init__(self, rdclass, rdtype, data): + super().__init__(rdclass, rdtype) + self.data = self._as_bytes(data) + + def to_text(self, origin=None, relativize=True, **kw): + return dns.rdata._base64ify(self.data, **kw) # pyright: ignore + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + b64 = tok.concatenate_remaining_identifiers().encode() + data = base64.b64decode(b64) + return cls(rdclass, rdtype, data) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(self.data) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + data = parser.get_remaining() + return cls(rdclass, rdtype, data) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/HTTPS.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/HTTPS.py new file mode 100644 index 0000000..15464cb --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/HTTPS.py @@ -0,0 +1,9 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import dns.immutable +import dns.rdtypes.svcbbase + + +@dns.immutable.immutable +class HTTPS(dns.rdtypes.svcbbase.SVCBBase): + """HTTPS record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/IPSECKEY.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/IPSECKEY.py new file mode 100644 index 0000000..aef93ae --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/IPSECKEY.py @@ -0,0 +1,87 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 +import struct + +import dns.exception +import dns.immutable +import dns.rdata +import dns.rdtypes.util + + +class Gateway(dns.rdtypes.util.Gateway): + name = "IPSECKEY gateway" + + +@dns.immutable.immutable +class IPSECKEY(dns.rdata.Rdata): + """IPSECKEY record""" + + # see: RFC 4025 + + __slots__ = ["precedence", "gateway_type", "algorithm", "gateway", "key"] + + def __init__( + self, rdclass, rdtype, precedence, gateway_type, algorithm, gateway, key + ): + super().__init__(rdclass, rdtype) + gateway = Gateway(gateway_type, gateway) + self.precedence = self._as_uint8(precedence) + self.gateway_type = gateway.type + self.algorithm = self._as_uint8(algorithm) + self.gateway = gateway.gateway + self.key = self._as_bytes(key) + + def to_text(self, origin=None, relativize=True, **kw): + gateway = Gateway(self.gateway_type, self.gateway).to_text(origin, relativize) + key = dns.rdata._base64ify(self.key, **kw) # pyright: ignore + return f"{self.precedence} {self.gateway_type} {self.algorithm} {gateway} {key}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + precedence = tok.get_uint8() + gateway_type = tok.get_uint8() + algorithm = tok.get_uint8() + gateway = Gateway.from_text( + gateway_type, tok, origin, relativize, relativize_to + ) + b64 = tok.concatenate_remaining_identifiers().encode() + key = base64.b64decode(b64) + return cls( + rdclass, rdtype, precedence, gateway_type, algorithm, gateway.gateway, key + ) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + header = struct.pack("!BBB", self.precedence, self.gateway_type, self.algorithm) + file.write(header) + Gateway(self.gateway_type, self.gateway).to_wire( + file, compress, origin, canonicalize + ) + file.write(self.key) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct("!BBB") + gateway_type = header[1] + gateway = Gateway.from_wire_parser(gateway_type, parser, origin) + key = parser.get_remaining() + return cls( + rdclass, rdtype, header[0], gateway_type, header[2], gateway.gateway, key + ) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/KX.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/KX.py new file mode 100644 index 0000000..6073df4 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/KX.py @@ -0,0 +1,24 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.mxbase + + +@dns.immutable.immutable +class KX(dns.rdtypes.mxbase.UncompressedDowncasingMX): + """KX record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NAPTR.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NAPTR.py new file mode 100644 index 0000000..98bbf4a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NAPTR.py @@ -0,0 +1,109 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata +import dns.rdtypes.util + + +def _write_string(file, s): + l = len(s) + assert l < 256 + file.write(struct.pack("!B", l)) + file.write(s) + + +@dns.immutable.immutable +class NAPTR(dns.rdata.Rdata): + """NAPTR record""" + + # see: RFC 3403 + + __slots__ = ["order", "preference", "flags", "service", "regexp", "replacement"] + + def __init__( + self, rdclass, rdtype, order, preference, flags, service, regexp, replacement + ): + super().__init__(rdclass, rdtype) + self.flags = self._as_bytes(flags, True, 255) + self.service = self._as_bytes(service, True, 255) + self.regexp = self._as_bytes(regexp, True, 255) + self.order = self._as_uint16(order) + self.preference = self._as_uint16(preference) + self.replacement = self._as_name(replacement) + + def to_text(self, origin=None, relativize=True, **kw): + replacement = self.replacement.choose_relativity(origin, relativize) + return ( + f"{self.order} {self.preference} " + f'"{dns.rdata._escapify(self.flags)}" ' + f'"{dns.rdata._escapify(self.service)}" ' + f'"{dns.rdata._escapify(self.regexp)}" ' + f"{replacement}" + ) + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + order = tok.get_uint16() + preference = tok.get_uint16() + flags = tok.get_string() + service = tok.get_string() + regexp = tok.get_string() + replacement = tok.get_name(origin, relativize, relativize_to) + return cls( + rdclass, rdtype, order, preference, flags, service, regexp, replacement + ) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + two_ints = struct.pack("!HH", self.order, self.preference) + file.write(two_ints) + _write_string(file, self.flags) + _write_string(file, self.service) + _write_string(file, self.regexp) + self.replacement.to_wire(file, compress, origin, canonicalize) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (order, preference) = parser.get_struct("!HH") + strings = [] + for _ in range(3): + s = parser.get_counted_bytes() + strings.append(s) + replacement = parser.get_name(origin) + return cls( + rdclass, + rdtype, + order, + preference, + strings[0], + strings[1], + strings[2], + replacement, + ) + + def _processing_priority(self): + return (self.order, self.preference) + + @classmethod + def _processing_order(cls, iterable): + return dns.rdtypes.util.priority_processing_order(iterable) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP.py new file mode 100644 index 0000000..d55edb7 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP.py @@ -0,0 +1,60 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import binascii + +import dns.exception +import dns.immutable +import dns.rdata +import dns.tokenizer + + +@dns.immutable.immutable +class NSAP(dns.rdata.Rdata): + """NSAP record.""" + + # see: RFC 1706 + + __slots__ = ["address"] + + def __init__(self, rdclass, rdtype, address): + super().__init__(rdclass, rdtype) + self.address = self._as_bytes(address) + + def to_text(self, origin=None, relativize=True, **kw): + return f"0x{binascii.hexlify(self.address).decode()}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + address = tok.get_string() + if address[0:2] != "0x": + raise dns.exception.SyntaxError("string does not start with 0x") + address = address[2:].replace(".", "") + if len(address) % 2 != 0: + raise dns.exception.SyntaxError("hexstring has odd length") + address = binascii.unhexlify(address.encode()) + return cls(rdclass, rdtype, address) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(self.address) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + address = parser.get_remaining() + return cls(rdclass, rdtype, address) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP_PTR.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP_PTR.py new file mode 100644 index 0000000..ce1c663 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP_PTR.py @@ -0,0 +1,24 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import dns.immutable +import dns.rdtypes.nsbase + + +@dns.immutable.immutable +class NSAP_PTR(dns.rdtypes.nsbase.UncompressedNS): + """NSAP-PTR record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/PX.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/PX.py new file mode 100644 index 0000000..20143bf --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/PX.py @@ -0,0 +1,73 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata +import dns.rdtypes.util + + +@dns.immutable.immutable +class PX(dns.rdata.Rdata): + """PX record.""" + + # see: RFC 2163 + + __slots__ = ["preference", "map822", "mapx400"] + + def __init__(self, rdclass, rdtype, preference, map822, mapx400): + super().__init__(rdclass, rdtype) + self.preference = self._as_uint16(preference) + self.map822 = self._as_name(map822) + self.mapx400 = self._as_name(mapx400) + + def to_text(self, origin=None, relativize=True, **kw): + map822 = self.map822.choose_relativity(origin, relativize) + mapx400 = self.mapx400.choose_relativity(origin, relativize) + return f"{self.preference} {map822} {mapx400}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + preference = tok.get_uint16() + map822 = tok.get_name(origin, relativize, relativize_to) + mapx400 = tok.get_name(origin, relativize, relativize_to) + return cls(rdclass, rdtype, preference, map822, mapx400) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + pref = struct.pack("!H", self.preference) + file.write(pref) + self.map822.to_wire(file, None, origin, canonicalize) + self.mapx400.to_wire(file, None, origin, canonicalize) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + preference = parser.get_uint16() + map822 = parser.get_name(origin) + mapx400 = parser.get_name(origin) + return cls(rdclass, rdtype, preference, map822, mapx400) + + def _processing_priority(self): + return self.preference + + @classmethod + def _processing_order(cls, iterable): + return dns.rdtypes.util.priority_processing_order(iterable) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/SRV.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/SRV.py new file mode 100644 index 0000000..044c10e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/SRV.py @@ -0,0 +1,75 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import struct + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata +import dns.rdtypes.util + + +@dns.immutable.immutable +class SRV(dns.rdata.Rdata): + """SRV record""" + + # see: RFC 2782 + + __slots__ = ["priority", "weight", "port", "target"] + + def __init__(self, rdclass, rdtype, priority, weight, port, target): + super().__init__(rdclass, rdtype) + self.priority = self._as_uint16(priority) + self.weight = self._as_uint16(weight) + self.port = self._as_uint16(port) + self.target = self._as_name(target) + + def to_text(self, origin=None, relativize=True, **kw): + target = self.target.choose_relativity(origin, relativize) + return f"{self.priority} {self.weight} {self.port} {target}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + priority = tok.get_uint16() + weight = tok.get_uint16() + port = tok.get_uint16() + target = tok.get_name(origin, relativize, relativize_to) + return cls(rdclass, rdtype, priority, weight, port, target) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + three_ints = struct.pack("!HHH", self.priority, self.weight, self.port) + file.write(three_ints) + self.target.to_wire(file, compress, origin, canonicalize) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + (priority, weight, port) = parser.get_struct("!HHH") + target = parser.get_name(origin) + return cls(rdclass, rdtype, priority, weight, port, target) + + def _processing_priority(self): + return self.priority + + def _processing_weight(self): + return self.weight + + @classmethod + def _processing_order(cls, iterable): + return dns.rdtypes.util.weighted_processing_order(iterable) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/SVCB.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/SVCB.py new file mode 100644 index 0000000..ff3e932 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/SVCB.py @@ -0,0 +1,9 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import dns.immutable +import dns.rdtypes.svcbbase + + +@dns.immutable.immutable +class SVCB(dns.rdtypes.svcbbase.SVCBBase): + """SVCB record""" diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/WKS.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/WKS.py new file mode 100644 index 0000000..cc6c373 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/WKS.py @@ -0,0 +1,100 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import socket +import struct + +import dns.immutable +import dns.ipv4 +import dns.rdata + +try: + _proto_tcp = socket.getprotobyname("tcp") + _proto_udp = socket.getprotobyname("udp") +except OSError: + # Fall back to defaults in case /etc/protocols is unavailable. + _proto_tcp = 6 + _proto_udp = 17 + + +@dns.immutable.immutable +class WKS(dns.rdata.Rdata): + """WKS record""" + + # see: RFC 1035 + + __slots__ = ["address", "protocol", "bitmap"] + + def __init__(self, rdclass, rdtype, address, protocol, bitmap): + super().__init__(rdclass, rdtype) + self.address = self._as_ipv4_address(address) + self.protocol = self._as_uint8(protocol) + self.bitmap = self._as_bytes(bitmap) + + def to_text(self, origin=None, relativize=True, **kw): + bits = [] + for i, byte in enumerate(self.bitmap): + for j in range(0, 8): + if byte & (0x80 >> j): + bits.append(str(i * 8 + j)) + text = " ".join(bits) + return f"{self.address} {self.protocol} {text}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + address = tok.get_string() + protocol = tok.get_string() + if protocol.isdigit(): + protocol = int(protocol) + else: + protocol = socket.getprotobyname(protocol) + bitmap = bytearray() + for token in tok.get_remaining(): + value = token.unescape().value + if value.isdigit(): + serv = int(value) + else: + if protocol != _proto_udp and protocol != _proto_tcp: + raise NotImplementedError("protocol must be TCP or UDP") + if protocol == _proto_udp: + protocol_text = "udp" + else: + protocol_text = "tcp" + serv = socket.getservbyname(value, protocol_text) + i = serv // 8 + l = len(bitmap) + if l < i + 1: + for _ in range(l, i + 1): + bitmap.append(0) + bitmap[i] = bitmap[i] | (0x80 >> (serv % 8)) + bitmap = dns.rdata._truncate_bitmap(bitmap) + return cls(rdclass, rdtype, address, protocol, bitmap) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(dns.ipv4.inet_aton(self.address)) + protocol = struct.pack("!B", self.protocol) + file.write(protocol) + file.write(self.bitmap) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + address = parser.get_bytes(4) + protocol = parser.get_uint8() + bitmap = parser.get_remaining() + return cls(rdclass, rdtype, address, protocol, bitmap) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/__init__.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/__init__.py new file mode 100644 index 0000000..dcec4dd --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/IN/__init__.py @@ -0,0 +1,35 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Class IN rdata type classes.""" + +__all__ = [ + "A", + "AAAA", + "APL", + "DHCID", + "HTTPS", + "IPSECKEY", + "KX", + "NAPTR", + "NSAP", + "NSAP_PTR", + "PX", + "SRV", + "SVCB", + "WKS", +] diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/__init__.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/__init__.py new file mode 100644 index 0000000..3997f84 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/__init__.py @@ -0,0 +1,33 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS rdata type classes""" + +__all__ = [ + "ANY", + "IN", + "CH", + "dnskeybase", + "dsbase", + "euibase", + "mxbase", + "nsbase", + "svcbbase", + "tlsabase", + "txtbase", + "util", +] diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/dnskeybase.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/dnskeybase.py new file mode 100644 index 0000000..fb49f92 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/dnskeybase.py @@ -0,0 +1,83 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2004-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import base64 +import enum +import struct + +import dns.dnssectypes +import dns.exception +import dns.immutable +import dns.rdata + +# wildcard import +__all__ = ["SEP", "REVOKE", "ZONE"] # noqa: F822 + + +class Flag(enum.IntFlag): + SEP = 0x0001 + REVOKE = 0x0080 + ZONE = 0x0100 + + +@dns.immutable.immutable +class DNSKEYBase(dns.rdata.Rdata): + """Base class for rdata that is like a DNSKEY record""" + + __slots__ = ["flags", "protocol", "algorithm", "key"] + + def __init__(self, rdclass, rdtype, flags, protocol, algorithm, key): + super().__init__(rdclass, rdtype) + self.flags = Flag(self._as_uint16(flags)) + self.protocol = self._as_uint8(protocol) + self.algorithm = dns.dnssectypes.Algorithm.make(algorithm) + self.key = self._as_bytes(key) + + def to_text(self, origin=None, relativize=True, **kw): + key = dns.rdata._base64ify(self.key, **kw) # pyright: ignore + return f"{self.flags} {self.protocol} {self.algorithm} {key}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + flags = tok.get_uint16() + protocol = tok.get_uint8() + algorithm = tok.get_string() + b64 = tok.concatenate_remaining_identifiers().encode() + key = base64.b64decode(b64) + return cls(rdclass, rdtype, flags, protocol, algorithm, key) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + header = struct.pack("!HBB", self.flags, self.protocol, self.algorithm) + file.write(header) + file.write(self.key) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct("!HBB") + key = parser.get_remaining() + return cls(rdclass, rdtype, header[0], header[1], header[2], key) + + +### BEGIN generated Flag constants + +SEP = Flag.SEP +REVOKE = Flag.REVOKE +ZONE = Flag.ZONE + +### END generated Flag constants diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/dsbase.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/dsbase.py new file mode 100644 index 0000000..8e05c2a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/dsbase.py @@ -0,0 +1,83 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2010, 2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import binascii +import struct + +import dns.dnssectypes +import dns.immutable +import dns.rdata +import dns.rdatatype + + +@dns.immutable.immutable +class DSBase(dns.rdata.Rdata): + """Base class for rdata that is like a DS record""" + + __slots__ = ["key_tag", "algorithm", "digest_type", "digest"] + + # Digest types registry: + # https://www.iana.org/assignments/ds-rr-types/ds-rr-types.xhtml + _digest_length_by_type = { + 1: 20, # SHA-1, RFC 3658 Sec. 2.4 + 2: 32, # SHA-256, RFC 4509 Sec. 2.2 + 3: 32, # GOST R 34.11-94, RFC 5933 Sec. 4 in conjunction with RFC 4490 Sec. 2.1 + 4: 48, # SHA-384, RFC 6605 Sec. 2 + } + + def __init__(self, rdclass, rdtype, key_tag, algorithm, digest_type, digest): + super().__init__(rdclass, rdtype) + self.key_tag = self._as_uint16(key_tag) + self.algorithm = dns.dnssectypes.Algorithm.make(algorithm) + self.digest_type = dns.dnssectypes.DSDigest.make(self._as_uint8(digest_type)) + self.digest = self._as_bytes(digest) + try: + if len(self.digest) != self._digest_length_by_type[self.digest_type]: + raise ValueError("digest length inconsistent with digest type") + except KeyError: + if self.digest_type == 0: # reserved, RFC 3658 Sec. 2.4 + raise ValueError("digest type 0 is reserved") + + def to_text(self, origin=None, relativize=True, **kw): + kw = kw.copy() + chunksize = kw.pop("chunksize", 128) + digest = dns.rdata._hexify( + self.digest, chunksize=chunksize, **kw # pyright: ignore + ) + return f"{self.key_tag} {self.algorithm} {self.digest_type} {digest}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + key_tag = tok.get_uint16() + algorithm = tok.get_string() + digest_type = tok.get_uint8() + digest = tok.concatenate_remaining_identifiers().encode() + digest = binascii.unhexlify(digest) + return cls(rdclass, rdtype, key_tag, algorithm, digest_type, digest) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + header = struct.pack("!HBB", self.key_tag, self.algorithm, self.digest_type) + file.write(header) + file.write(self.digest) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct("!HBB") + digest = parser.get_remaining() + return cls(rdclass, rdtype, header[0], header[1], header[2], digest) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/euibase.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/euibase.py new file mode 100644 index 0000000..4eb82eb --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/euibase.py @@ -0,0 +1,73 @@ +# Copyright (C) 2015 Red Hat, Inc. +# Author: Petr Spacek +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED 'AS IS' AND RED HAT DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import binascii + +import dns.exception +import dns.immutable +import dns.rdata + + +@dns.immutable.immutable +class EUIBase(dns.rdata.Rdata): + """EUIxx record""" + + # see: rfc7043.txt + + __slots__ = ["eui"] + # redefine these in subclasses + byte_len = 0 + text_len = 0 + # byte_len = 6 # 0123456789ab (in hex) + # text_len = byte_len * 3 - 1 # 01-23-45-67-89-ab + + def __init__(self, rdclass, rdtype, eui): + super().__init__(rdclass, rdtype) + self.eui = self._as_bytes(eui) + if len(self.eui) != self.byte_len: + raise dns.exception.FormError( + f"EUI{self.byte_len * 8} rdata has to have {self.byte_len} bytes" + ) + + def to_text(self, origin=None, relativize=True, **kw): + return dns.rdata._hexify(self.eui, chunksize=2, separator=b"-", **kw) + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + text = tok.get_string() + if len(text) != cls.text_len: + raise dns.exception.SyntaxError( + f"Input text must have {cls.text_len} characters" + ) + for i in range(2, cls.byte_len * 3 - 1, 3): + if text[i] != "-": + raise dns.exception.SyntaxError(f"Dash expected at position {i}") + text = text.replace("-", "") + try: + data = binascii.unhexlify(text.encode()) + except (ValueError, TypeError) as ex: + raise dns.exception.SyntaxError(f"Hex decoding error: {str(ex)}") + return cls(rdclass, rdtype, data) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(self.eui) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + eui = parser.get_bytes(cls.byte_len) + return cls(rdclass, rdtype, eui) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/mxbase.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/mxbase.py new file mode 100644 index 0000000..5d33e61 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/mxbase.py @@ -0,0 +1,87 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""MX-like base classes.""" + +import struct + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata +import dns.rdtypes.util + + +@dns.immutable.immutable +class MXBase(dns.rdata.Rdata): + """Base class for rdata that is like an MX record.""" + + __slots__ = ["preference", "exchange"] + + def __init__(self, rdclass, rdtype, preference, exchange): + super().__init__(rdclass, rdtype) + self.preference = self._as_uint16(preference) + self.exchange = self._as_name(exchange) + + def to_text(self, origin=None, relativize=True, **kw): + exchange = self.exchange.choose_relativity(origin, relativize) + return f"{self.preference} {exchange}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + preference = tok.get_uint16() + exchange = tok.get_name(origin, relativize, relativize_to) + return cls(rdclass, rdtype, preference, exchange) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + pref = struct.pack("!H", self.preference) + file.write(pref) + self.exchange.to_wire(file, compress, origin, canonicalize) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + preference = parser.get_uint16() + exchange = parser.get_name(origin) + return cls(rdclass, rdtype, preference, exchange) + + def _processing_priority(self): + return self.preference + + @classmethod + def _processing_order(cls, iterable): + return dns.rdtypes.util.priority_processing_order(iterable) + + +@dns.immutable.immutable +class UncompressedMX(MXBase): + """Base class for rdata that is like an MX record, but whose name + is not compressed when converted to DNS wire format, and whose + digestable form is not downcased.""" + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + super()._to_wire(file, None, origin, False) + + +@dns.immutable.immutable +class UncompressedDowncasingMX(MXBase): + """Base class for rdata that is like an MX record, but whose name + is not compressed when convert to DNS wire format.""" + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + super()._to_wire(file, None, origin, canonicalize) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/nsbase.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/nsbase.py new file mode 100644 index 0000000..904224f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/nsbase.py @@ -0,0 +1,63 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""NS-like base classes.""" + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata + + +@dns.immutable.immutable +class NSBase(dns.rdata.Rdata): + """Base class for rdata that is like an NS record.""" + + __slots__ = ["target"] + + def __init__(self, rdclass, rdtype, target): + super().__init__(rdclass, rdtype) + self.target = self._as_name(target) + + def to_text(self, origin=None, relativize=True, **kw): + target = self.target.choose_relativity(origin, relativize) + return str(target) + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + target = tok.get_name(origin, relativize, relativize_to) + return cls(rdclass, rdtype, target) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.target.to_wire(file, compress, origin, canonicalize) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + target = parser.get_name(origin) + return cls(rdclass, rdtype, target) + + +@dns.immutable.immutable +class UncompressedNS(NSBase): + """Base class for rdata that is like an NS record, but whose name + is not compressed when convert to DNS wire format, and whose + digestable form is not downcased.""" + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + self.target.to_wire(file, None, origin, False) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/svcbbase.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/svcbbase.py new file mode 100644 index 0000000..7338b66 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/svcbbase.py @@ -0,0 +1,587 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import base64 +import enum +import struct +from typing import Any, Dict + +import dns.enum +import dns.exception +import dns.immutable +import dns.ipv4 +import dns.ipv6 +import dns.name +import dns.rdata +import dns.rdtypes.util +import dns.renderer +import dns.tokenizer +import dns.wire + +# Until there is an RFC, this module is experimental and may be changed in +# incompatible ways. + + +class UnknownParamKey(dns.exception.DNSException): + """Unknown SVCB ParamKey""" + + +class ParamKey(dns.enum.IntEnum): + """SVCB ParamKey""" + + MANDATORY = 0 + ALPN = 1 + NO_DEFAULT_ALPN = 2 + PORT = 3 + IPV4HINT = 4 + ECH = 5 + IPV6HINT = 6 + DOHPATH = 7 + OHTTP = 8 + + @classmethod + def _maximum(cls): + return 65535 + + @classmethod + def _short_name(cls): + return "SVCBParamKey" + + @classmethod + def _prefix(cls): + return "KEY" + + @classmethod + def _unknown_exception_class(cls): + return UnknownParamKey + + +class Emptiness(enum.IntEnum): + NEVER = 0 + ALWAYS = 1 + ALLOWED = 2 + + +def _validate_key(key): + force_generic = False + if isinstance(key, bytes): + # We decode to latin-1 so we get 0-255 as valid and do NOT interpret + # UTF-8 sequences + key = key.decode("latin-1") + if isinstance(key, str): + if key.lower().startswith("key"): + force_generic = True + if key[3:].startswith("0") and len(key) != 4: + # key has leading zeros + raise ValueError("leading zeros in key") + key = key.replace("-", "_") + return (ParamKey.make(key), force_generic) + + +def key_to_text(key): + return ParamKey.to_text(key).replace("_", "-").lower() + + +# Like rdata escapify, but escapes ',' too. + +_escaped = b'",\\' + + +def _escapify(qstring): + text = "" + for c in qstring: + if c in _escaped: + text += "\\" + chr(c) + elif c >= 0x20 and c < 0x7F: + text += chr(c) + else: + text += f"\\{c:03d}" + return text + + +def _unescape(value: str) -> bytes: + if value == "": + return b"" + unescaped = b"" + l = len(value) + i = 0 + while i < l: + c = value[i] + i += 1 + if c == "\\": + if i >= l: # pragma: no cover (can't happen via tokenizer get()) + raise dns.exception.UnexpectedEnd + c = value[i] + i += 1 + if c.isdigit(): + if i >= l: + raise dns.exception.UnexpectedEnd + c2 = value[i] + i += 1 + if i >= l: + raise dns.exception.UnexpectedEnd + c3 = value[i] + i += 1 + if not (c2.isdigit() and c3.isdigit()): + raise dns.exception.SyntaxError + codepoint = int(c) * 100 + int(c2) * 10 + int(c3) + if codepoint > 255: + raise dns.exception.SyntaxError + unescaped += b"%c" % (codepoint) + continue + unescaped += c.encode() + return unescaped + + +def _split(value): + l = len(value) + i = 0 + items = [] + unescaped = b"" + while i < l: + c = value[i] + i += 1 + if c == ord("\\"): + if i >= l: # pragma: no cover (can't happen via tokenizer get()) + raise dns.exception.UnexpectedEnd + c = value[i] + i += 1 + unescaped += b"%c" % (c) + elif c == ord(","): + items.append(unescaped) + unescaped = b"" + else: + unescaped += b"%c" % (c) + items.append(unescaped) + return items + + +@dns.immutable.immutable +class Param: + """Abstract base class for SVCB parameters""" + + @classmethod + def emptiness(cls) -> Emptiness: + return Emptiness.NEVER + + +@dns.immutable.immutable +class GenericParam(Param): + """Generic SVCB parameter""" + + def __init__(self, value): + self.value = dns.rdata.Rdata._as_bytes(value, True) + + @classmethod + def emptiness(cls): + return Emptiness.ALLOWED + + @classmethod + def from_value(cls, value): + if value is None or len(value) == 0: + return None + else: + return cls(_unescape(value)) + + def to_text(self): + return '"' + dns.rdata._escapify(self.value) + '"' + + @classmethod + def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613 + value = parser.get_bytes(parser.remaining()) + if len(value) == 0: + return None + else: + return cls(value) + + def to_wire(self, file, origin=None): # pylint: disable=W0613 + file.write(self.value) + + +@dns.immutable.immutable +class MandatoryParam(Param): + def __init__(self, keys): + # check for duplicates + keys = sorted([_validate_key(key)[0] for key in keys]) + prior_k = None + for k in keys: + if k == prior_k: + raise ValueError(f"duplicate key {k:d}") + prior_k = k + if k == ParamKey.MANDATORY: + raise ValueError("listed the mandatory key as mandatory") + self.keys = tuple(keys) + + @classmethod + def from_value(cls, value): + keys = [k.encode() for k in value.split(",")] + return cls(keys) + + def to_text(self): + return '"' + ",".join([key_to_text(key) for key in self.keys]) + '"' + + @classmethod + def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613 + keys = [] + last_key = -1 + while parser.remaining() > 0: + key = parser.get_uint16() + if key < last_key: + raise dns.exception.FormError("manadatory keys not ascending") + last_key = key + keys.append(key) + return cls(keys) + + def to_wire(self, file, origin=None): # pylint: disable=W0613 + for key in self.keys: + file.write(struct.pack("!H", key)) + + +@dns.immutable.immutable +class ALPNParam(Param): + def __init__(self, ids): + self.ids = dns.rdata.Rdata._as_tuple( + ids, lambda x: dns.rdata.Rdata._as_bytes(x, True, 255, False) + ) + + @classmethod + def from_value(cls, value): + return cls(_split(_unescape(value))) + + def to_text(self): + value = ",".join([_escapify(id) for id in self.ids]) + return '"' + dns.rdata._escapify(value.encode()) + '"' + + @classmethod + def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613 + ids = [] + while parser.remaining() > 0: + id = parser.get_counted_bytes() + ids.append(id) + return cls(ids) + + def to_wire(self, file, origin=None): # pylint: disable=W0613 + for id in self.ids: + file.write(struct.pack("!B", len(id))) + file.write(id) + + +@dns.immutable.immutable +class NoDefaultALPNParam(Param): + # We don't ever expect to instantiate this class, but we need + # a from_value() and a from_wire_parser(), so we just return None + # from the class methods when things are OK. + + @classmethod + def emptiness(cls): + return Emptiness.ALWAYS + + @classmethod + def from_value(cls, value): + if value is None or value == "": + return None + else: + raise ValueError("no-default-alpn with non-empty value") + + def to_text(self): + raise NotImplementedError # pragma: no cover + + @classmethod + def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613 + if parser.remaining() != 0: + raise dns.exception.FormError + return None + + def to_wire(self, file, origin=None): # pylint: disable=W0613 + raise NotImplementedError # pragma: no cover + + +@dns.immutable.immutable +class PortParam(Param): + def __init__(self, port): + self.port = dns.rdata.Rdata._as_uint16(port) + + @classmethod + def from_value(cls, value): + value = int(value) + return cls(value) + + def to_text(self): + return f'"{self.port}"' + + @classmethod + def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613 + port = parser.get_uint16() + return cls(port) + + def to_wire(self, file, origin=None): # pylint: disable=W0613 + file.write(struct.pack("!H", self.port)) + + +@dns.immutable.immutable +class IPv4HintParam(Param): + def __init__(self, addresses): + self.addresses = dns.rdata.Rdata._as_tuple( + addresses, dns.rdata.Rdata._as_ipv4_address + ) + + @classmethod + def from_value(cls, value): + addresses = value.split(",") + return cls(addresses) + + def to_text(self): + return '"' + ",".join(self.addresses) + '"' + + @classmethod + def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613 + addresses = [] + while parser.remaining() > 0: + ip = parser.get_bytes(4) + addresses.append(dns.ipv4.inet_ntoa(ip)) + return cls(addresses) + + def to_wire(self, file, origin=None): # pylint: disable=W0613 + for address in self.addresses: + file.write(dns.ipv4.inet_aton(address)) + + +@dns.immutable.immutable +class IPv6HintParam(Param): + def __init__(self, addresses): + self.addresses = dns.rdata.Rdata._as_tuple( + addresses, dns.rdata.Rdata._as_ipv6_address + ) + + @classmethod + def from_value(cls, value): + addresses = value.split(",") + return cls(addresses) + + def to_text(self): + return '"' + ",".join(self.addresses) + '"' + + @classmethod + def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613 + addresses = [] + while parser.remaining() > 0: + ip = parser.get_bytes(16) + addresses.append(dns.ipv6.inet_ntoa(ip)) + return cls(addresses) + + def to_wire(self, file, origin=None): # pylint: disable=W0613 + for address in self.addresses: + file.write(dns.ipv6.inet_aton(address)) + + +@dns.immutable.immutable +class ECHParam(Param): + def __init__(self, ech): + self.ech = dns.rdata.Rdata._as_bytes(ech, True) + + @classmethod + def from_value(cls, value): + if "\\" in value: + raise ValueError("escape in ECH value") + value = base64.b64decode(value.encode()) + return cls(value) + + def to_text(self): + b64 = base64.b64encode(self.ech).decode("ascii") + return f'"{b64}"' + + @classmethod + def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613 + value = parser.get_bytes(parser.remaining()) + return cls(value) + + def to_wire(self, file, origin=None): # pylint: disable=W0613 + file.write(self.ech) + + +@dns.immutable.immutable +class OHTTPParam(Param): + # We don't ever expect to instantiate this class, but we need + # a from_value() and a from_wire_parser(), so we just return None + # from the class methods when things are OK. + + @classmethod + def emptiness(cls): + return Emptiness.ALWAYS + + @classmethod + def from_value(cls, value): + if value is None or value == "": + return None + else: + raise ValueError("ohttp with non-empty value") + + def to_text(self): + raise NotImplementedError # pragma: no cover + + @classmethod + def from_wire_parser(cls, parser, origin=None): # pylint: disable=W0613 + if parser.remaining() != 0: + raise dns.exception.FormError + return None + + def to_wire(self, file, origin=None): # pylint: disable=W0613 + raise NotImplementedError # pragma: no cover + + +_class_for_key: Dict[ParamKey, Any] = { + ParamKey.MANDATORY: MandatoryParam, + ParamKey.ALPN: ALPNParam, + ParamKey.NO_DEFAULT_ALPN: NoDefaultALPNParam, + ParamKey.PORT: PortParam, + ParamKey.IPV4HINT: IPv4HintParam, + ParamKey.ECH: ECHParam, + ParamKey.IPV6HINT: IPv6HintParam, + ParamKey.OHTTP: OHTTPParam, +} + + +def _validate_and_define(params, key, value): + (key, force_generic) = _validate_key(_unescape(key)) + if key in params: + raise SyntaxError(f'duplicate key "{key:d}"') + cls = _class_for_key.get(key, GenericParam) + emptiness = cls.emptiness() + if value is None: + if emptiness == Emptiness.NEVER: + raise SyntaxError("value cannot be empty") + value = cls.from_value(value) + else: + if force_generic: + value = cls.from_wire_parser(dns.wire.Parser(_unescape(value))) + else: + value = cls.from_value(value) + params[key] = value + + +@dns.immutable.immutable +class SVCBBase(dns.rdata.Rdata): + """Base class for SVCB-like records""" + + # see: draft-ietf-dnsop-svcb-https-11 + + __slots__ = ["priority", "target", "params"] + + def __init__(self, rdclass, rdtype, priority, target, params): + super().__init__(rdclass, rdtype) + self.priority = self._as_uint16(priority) + self.target = self._as_name(target) + for k, v in params.items(): + k = ParamKey.make(k) + if not isinstance(v, Param) and v is not None: + raise ValueError(f"{k:d} not a Param") + self.params = dns.immutable.Dict(params) + # Make sure any parameter listed as mandatory is present in the + # record. + mandatory = params.get(ParamKey.MANDATORY) + if mandatory: + for key in mandatory.keys: + # Note we have to say "not in" as we have None as a value + # so a get() and a not None test would be wrong. + if key not in params: + raise ValueError(f"key {key:d} declared mandatory but not present") + # The no-default-alpn parameter requires the alpn parameter. + if ParamKey.NO_DEFAULT_ALPN in params: + if ParamKey.ALPN not in params: + raise ValueError("no-default-alpn present, but alpn missing") + + def to_text(self, origin=None, relativize=True, **kw): + target = self.target.choose_relativity(origin, relativize) + params = [] + for key in sorted(self.params.keys()): + value = self.params[key] + if value is None: + params.append(key_to_text(key)) + else: + kv = key_to_text(key) + "=" + value.to_text() + params.append(kv) + if len(params) > 0: + space = " " + else: + space = "" + return f"{self.priority} {target}{space}{' '.join(params)}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + priority = tok.get_uint16() + target = tok.get_name(origin, relativize, relativize_to) + if priority == 0: + token = tok.get() + if not token.is_eol_or_eof(): + raise SyntaxError("parameters in AliasMode") + tok.unget(token) + params = {} + while True: + token = tok.get() + if token.is_eol_or_eof(): + tok.unget(token) + break + if token.ttype != dns.tokenizer.IDENTIFIER: + raise SyntaxError("parameter is not an identifier") + equals = token.value.find("=") + if equals == len(token.value) - 1: + # 'key=', so next token should be a quoted string without + # any intervening whitespace. + key = token.value[:-1] + token = tok.get(want_leading=True) + if token.ttype != dns.tokenizer.QUOTED_STRING: + raise SyntaxError("whitespace after =") + value = token.value + elif equals > 0: + # key=value + key = token.value[:equals] + value = token.value[equals + 1 :] + elif equals == 0: + # =key + raise SyntaxError('parameter cannot start with "="') + else: + # key + key = token.value + value = None + _validate_and_define(params, key, value) + return cls(rdclass, rdtype, priority, target, params) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + file.write(struct.pack("!H", self.priority)) + self.target.to_wire(file, None, origin, False) + for key in sorted(self.params): + file.write(struct.pack("!H", key)) + value = self.params[key] + with dns.renderer.prefixed_length(file, 2): + # Note that we're still writing a length of zero if the value is None + if value is not None: + value.to_wire(file, origin) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + priority = parser.get_uint16() + target = parser.get_name(origin) + if priority == 0 and parser.remaining() != 0: + raise dns.exception.FormError("parameters in AliasMode") + params = {} + prior_key = -1 + while parser.remaining() > 0: + key = parser.get_uint16() + if key < prior_key: + raise dns.exception.FormError("keys not in order") + prior_key = key + vlen = parser.get_uint16() + pkey = ParamKey.make(key) + pcls = _class_for_key.get(pkey, GenericParam) + with parser.restrict_to(vlen): + value = pcls.from_wire_parser(parser, origin) + params[pkey] = value + return cls(rdclass, rdtype, priority, target, params) + + def _processing_priority(self): + return self.priority + + @classmethod + def _processing_order(cls, iterable): + return dns.rdtypes.util.priority_processing_order(iterable) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/tlsabase.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/tlsabase.py new file mode 100644 index 0000000..ddc196f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/tlsabase.py @@ -0,0 +1,69 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2005-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import binascii +import struct + +import dns.immutable +import dns.rdata +import dns.rdatatype + + +@dns.immutable.immutable +class TLSABase(dns.rdata.Rdata): + """Base class for TLSA and SMIMEA records""" + + # see: RFC 6698 + + __slots__ = ["usage", "selector", "mtype", "cert"] + + def __init__(self, rdclass, rdtype, usage, selector, mtype, cert): + super().__init__(rdclass, rdtype) + self.usage = self._as_uint8(usage) + self.selector = self._as_uint8(selector) + self.mtype = self._as_uint8(mtype) + self.cert = self._as_bytes(cert) + + def to_text(self, origin=None, relativize=True, **kw): + kw = kw.copy() + chunksize = kw.pop("chunksize", 128) + cert = dns.rdata._hexify( + self.cert, chunksize=chunksize, **kw # pyright: ignore + ) + return f"{self.usage} {self.selector} {self.mtype} {cert}" + + @classmethod + def from_text( + cls, rdclass, rdtype, tok, origin=None, relativize=True, relativize_to=None + ): + usage = tok.get_uint8() + selector = tok.get_uint8() + mtype = tok.get_uint8() + cert = tok.concatenate_remaining_identifiers().encode() + cert = binascii.unhexlify(cert) + return cls(rdclass, rdtype, usage, selector, mtype, cert) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + header = struct.pack("!BBB", self.usage, self.selector, self.mtype) + file.write(header) + file.write(self.cert) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + header = parser.get_struct("BBB") + cert = parser.get_remaining() + return cls(rdclass, rdtype, header[0], header[1], header[2], cert) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/txtbase.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/txtbase.py new file mode 100644 index 0000000..5e5b24f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/txtbase.py @@ -0,0 +1,109 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2006-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""TXT-like base class.""" + +from typing import Any, Dict, Iterable, Tuple + +import dns.exception +import dns.immutable +import dns.name +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.renderer +import dns.tokenizer + + +@dns.immutable.immutable +class TXTBase(dns.rdata.Rdata): + """Base class for rdata that is like a TXT record (see RFC 1035).""" + + __slots__ = ["strings"] + + def __init__( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + strings: Iterable[bytes | str], + ): + """Initialize a TXT-like rdata. + + *rdclass*, an ``int`` is the rdataclass of the Rdata. + + *rdtype*, an ``int`` is the rdatatype of the Rdata. + + *strings*, a tuple of ``bytes`` + """ + super().__init__(rdclass, rdtype) + self.strings: Tuple[bytes] = self._as_tuple( + strings, lambda x: self._as_bytes(x, True, 255) + ) + if len(self.strings) == 0: + raise ValueError("the list of strings must not be empty") + + def to_text( + self, + origin: dns.name.Name | None = None, + relativize: bool = True, + **kw: Dict[str, Any], + ) -> str: + txt = "" + prefix = "" + for s in self.strings: + txt += f'{prefix}"{dns.rdata._escapify(s)}"' + prefix = " " + return txt + + @classmethod + def from_text( + cls, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + tok: dns.tokenizer.Tokenizer, + origin: dns.name.Name | None = None, + relativize: bool = True, + relativize_to: dns.name.Name | None = None, + ) -> dns.rdata.Rdata: + strings = [] + for token in tok.get_remaining(): + token = token.unescape_to_bytes() + # The 'if' below is always true in the current code, but we + # are leaving this check in in case things change some day. + if not ( + token.is_quoted_string() or token.is_identifier() + ): # pragma: no cover + raise dns.exception.SyntaxError("expected a string") + if len(token.value) > 255: + raise dns.exception.SyntaxError("string too long") + strings.append(token.value) + if len(strings) == 0: + raise dns.exception.UnexpectedEnd + return cls(rdclass, rdtype, strings) + + def _to_wire(self, file, compress=None, origin=None, canonicalize=False): + for s in self.strings: + with dns.renderer.prefixed_length(file, 1): + file.write(s) + + @classmethod + def from_wire_parser(cls, rdclass, rdtype, parser, origin=None): + strings = [] + while parser.remaining() > 0: + s = parser.get_counted_bytes() + strings.append(s) + return cls(rdclass, rdtype, strings) diff --git a/netdeploy/lib/python3.11/site-packages/dns/rdtypes/util.py b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/util.py new file mode 100644 index 0000000..c17b154 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rdtypes/util.py @@ -0,0 +1,269 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2006, 2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import collections +import random +import struct +from typing import Any, Iterable, List, Tuple + +import dns.exception +import dns.ipv4 +import dns.ipv6 +import dns.name +import dns.rdata +import dns.rdatatype +import dns.tokenizer +import dns.wire + + +class Gateway: + """A helper class for the IPSECKEY gateway and AMTRELAY relay fields""" + + name = "" + + def __init__(self, type: Any, gateway: str | dns.name.Name | None = None): + self.type = dns.rdata.Rdata._as_uint8(type) + self.gateway = gateway + self._check() + + @classmethod + def _invalid_type(cls, gateway_type): + return f"invalid {cls.name} type: {gateway_type}" + + def _check(self): + if self.type == 0: + if self.gateway not in (".", None): + raise SyntaxError(f"invalid {self.name} for type 0") + self.gateway = None + elif self.type == 1: + # check that it's OK + assert isinstance(self.gateway, str) + dns.ipv4.inet_aton(self.gateway) + elif self.type == 2: + # check that it's OK + assert isinstance(self.gateway, str) + dns.ipv6.inet_aton(self.gateway) + elif self.type == 3: + if not isinstance(self.gateway, dns.name.Name): + raise SyntaxError(f"invalid {self.name}; not a name") + else: + raise SyntaxError(self._invalid_type(self.type)) + + def to_text(self, origin=None, relativize=True): + if self.type == 0: + return "." + elif self.type in (1, 2): + return self.gateway + elif self.type == 3: + assert isinstance(self.gateway, dns.name.Name) + return str(self.gateway.choose_relativity(origin, relativize)) + else: + raise ValueError(self._invalid_type(self.type)) # pragma: no cover + + @classmethod + def from_text( + cls, gateway_type, tok, origin=None, relativize=True, relativize_to=None + ): + if gateway_type in (0, 1, 2): + gateway = tok.get_string() + elif gateway_type == 3: + gateway = tok.get_name(origin, relativize, relativize_to) + else: + raise dns.exception.SyntaxError( + cls._invalid_type(gateway_type) + ) # pragma: no cover + return cls(gateway_type, gateway) + + # pylint: disable=unused-argument + def to_wire(self, file, compress=None, origin=None, canonicalize=False): + if self.type == 0: + pass + elif self.type == 1: + assert isinstance(self.gateway, str) + file.write(dns.ipv4.inet_aton(self.gateway)) + elif self.type == 2: + assert isinstance(self.gateway, str) + file.write(dns.ipv6.inet_aton(self.gateway)) + elif self.type == 3: + assert isinstance(self.gateway, dns.name.Name) + self.gateway.to_wire(file, None, origin, False) + else: + raise ValueError(self._invalid_type(self.type)) # pragma: no cover + + # pylint: enable=unused-argument + + @classmethod + def from_wire_parser(cls, gateway_type, parser, origin=None): + if gateway_type == 0: + gateway = None + elif gateway_type == 1: + gateway = dns.ipv4.inet_ntoa(parser.get_bytes(4)) + elif gateway_type == 2: + gateway = dns.ipv6.inet_ntoa(parser.get_bytes(16)) + elif gateway_type == 3: + gateway = parser.get_name(origin) + else: + raise dns.exception.FormError(cls._invalid_type(gateway_type)) + return cls(gateway_type, gateway) + + +class Bitmap: + """A helper class for the NSEC/NSEC3/CSYNC type bitmaps""" + + type_name = "" + + def __init__(self, windows: Iterable[Tuple[int, bytes]] | None = None): + last_window = -1 + if windows is None: + windows = [] + self.windows = windows + for window, bitmap in self.windows: + if not isinstance(window, int): + raise ValueError(f"bad {self.type_name} window type") + if window <= last_window: + raise ValueError(f"bad {self.type_name} window order") + if window > 256: + raise ValueError(f"bad {self.type_name} window number") + last_window = window + if not isinstance(bitmap, bytes): + raise ValueError(f"bad {self.type_name} octets type") + if len(bitmap) == 0 or len(bitmap) > 32: + raise ValueError(f"bad {self.type_name} octets") + + def to_text(self) -> str: + text = "" + for window, bitmap in self.windows: + bits = [] + for i, byte in enumerate(bitmap): + for j in range(0, 8): + if byte & (0x80 >> j): + rdtype = dns.rdatatype.RdataType.make(window * 256 + i * 8 + j) + bits.append(dns.rdatatype.to_text(rdtype)) + text += " " + " ".join(bits) + return text + + @classmethod + def from_text(cls, tok: "dns.tokenizer.Tokenizer") -> "Bitmap": + rdtypes = [] + for token in tok.get_remaining(): + rdtype = dns.rdatatype.from_text(token.unescape().value) + if rdtype == 0: + raise dns.exception.SyntaxError(f"{cls.type_name} with bit 0") + rdtypes.append(rdtype) + return cls.from_rdtypes(rdtypes) + + @classmethod + def from_rdtypes(cls, rdtypes: List[dns.rdatatype.RdataType]) -> "Bitmap": + rdtypes = sorted(rdtypes) + window = 0 + octets = 0 + prior_rdtype = 0 + bitmap = bytearray(b"\0" * 32) + windows = [] + for rdtype in rdtypes: + if rdtype == prior_rdtype: + continue + prior_rdtype = rdtype + new_window = rdtype // 256 + if new_window != window: + if octets != 0: + windows.append((window, bytes(bitmap[0:octets]))) + bitmap = bytearray(b"\0" * 32) + window = new_window + offset = rdtype % 256 + byte = offset // 8 + bit = offset % 8 + octets = byte + 1 + bitmap[byte] = bitmap[byte] | (0x80 >> bit) + if octets != 0: + windows.append((window, bytes(bitmap[0:octets]))) + return cls(windows) + + def to_wire(self, file: Any) -> None: + for window, bitmap in self.windows: + file.write(struct.pack("!BB", window, len(bitmap))) + file.write(bitmap) + + @classmethod + def from_wire_parser(cls, parser: "dns.wire.Parser") -> "Bitmap": + windows = [] + while parser.remaining() > 0: + window = parser.get_uint8() + bitmap = parser.get_counted_bytes() + windows.append((window, bitmap)) + return cls(windows) + + +def _priority_table(items): + by_priority = collections.defaultdict(list) + for rdata in items: + by_priority[rdata._processing_priority()].append(rdata) + return by_priority + + +def priority_processing_order(iterable): + items = list(iterable) + if len(items) == 1: + return items + by_priority = _priority_table(items) + ordered = [] + for k in sorted(by_priority.keys()): + rdatas = by_priority[k] + random.shuffle(rdatas) + ordered.extend(rdatas) + return ordered + + +_no_weight = 0.1 + + +def weighted_processing_order(iterable): + items = list(iterable) + if len(items) == 1: + return items + by_priority = _priority_table(items) + ordered = [] + for k in sorted(by_priority.keys()): + rdatas = by_priority[k] + total = sum(rdata._processing_weight() or _no_weight for rdata in rdatas) + while len(rdatas) > 1: + r = random.uniform(0, total) + for n, rdata in enumerate(rdatas): # noqa: B007 + weight = rdata._processing_weight() or _no_weight + if weight > r: + break + r -= weight + total -= weight # pyright: ignore[reportPossiblyUnboundVariable] + # pylint: disable=undefined-loop-variable + ordered.append(rdata) # pyright: ignore[reportPossiblyUnboundVariable] + del rdatas[n] # pyright: ignore[reportPossiblyUnboundVariable] + ordered.append(rdatas[0]) + return ordered + + +def parse_formatted_hex(formatted, num_chunks, chunk_size, separator): + if len(formatted) != num_chunks * (chunk_size + 1) - 1: + raise ValueError("invalid formatted hex string") + value = b"" + for _ in range(num_chunks): + chunk = formatted[0:chunk_size] + value += int(chunk, 16).to_bytes(chunk_size // 2, "big") + formatted = formatted[chunk_size:] + if len(formatted) > 0 and formatted[0] != separator: + raise ValueError("invalid formatted hex string") + formatted = formatted[1:] + return value diff --git a/netdeploy/lib/python3.11/site-packages/dns/renderer.py b/netdeploy/lib/python3.11/site-packages/dns/renderer.py new file mode 100644 index 0000000..cc912b2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/renderer.py @@ -0,0 +1,355 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Help for building DNS wire format messages""" + +import contextlib +import io +import random +import struct +import time + +import dns.edns +import dns.exception +import dns.rdataclass +import dns.rdatatype +import dns.tsig + +# Note we can't import dns.message for cicularity reasons + +QUESTION = 0 +ANSWER = 1 +AUTHORITY = 2 +ADDITIONAL = 3 + + +@contextlib.contextmanager +def prefixed_length(output, length_length): + output.write(b"\00" * length_length) + start = output.tell() + yield + end = output.tell() + length = end - start + if length > 0: + try: + output.seek(start - length_length) + try: + output.write(length.to_bytes(length_length, "big")) + except OverflowError: + raise dns.exception.FormError + finally: + output.seek(end) + + +class Renderer: + """Helper class for building DNS wire-format messages. + + Most applications can use the higher-level L{dns.message.Message} + class and its to_wire() method to generate wire-format messages. + This class is for those applications which need finer control + over the generation of messages. + + Typical use:: + + r = dns.renderer.Renderer(id=1, flags=0x80, max_size=512) + r.add_question(qname, qtype, qclass) + r.add_rrset(dns.renderer.ANSWER, rrset_1) + r.add_rrset(dns.renderer.ANSWER, rrset_2) + r.add_rrset(dns.renderer.AUTHORITY, ns_rrset) + r.add_rrset(dns.renderer.ADDITIONAL, ad_rrset_1) + r.add_rrset(dns.renderer.ADDITIONAL, ad_rrset_2) + r.add_edns(0, 0, 4096) + r.write_header() + r.add_tsig(keyname, secret, 300, 1, 0, '', request_mac) + wire = r.get_wire() + + If padding is going to be used, then the OPT record MUST be + written after everything else in the additional section except for + the TSIG (if any). + + output, an io.BytesIO, where rendering is written + + id: the message id + + flags: the message flags + + max_size: the maximum size of the message + + origin: the origin to use when rendering relative names + + compress: the compression table + + section: an int, the section currently being rendered + + counts: list of the number of RRs in each section + + mac: the MAC of the rendered message (if TSIG was used) + """ + + def __init__(self, id=None, flags=0, max_size=65535, origin=None): + """Initialize a new renderer.""" + + self.output = io.BytesIO() + if id is None: + self.id = random.randint(0, 65535) + else: + self.id = id + self.flags = flags + self.max_size = max_size + self.origin = origin + self.compress = {} + self.section = QUESTION + self.counts = [0, 0, 0, 0] + self.output.write(b"\x00" * 12) + self.mac = "" + self.reserved = 0 + self.was_padded = False + + def _rollback(self, where): + """Truncate the output buffer at offset *where*, and remove any + compression table entries that pointed beyond the truncation + point. + """ + + self.output.seek(where) + self.output.truncate() + keys_to_delete = [] + for k, v in self.compress.items(): + if v >= where: + keys_to_delete.append(k) + for k in keys_to_delete: + del self.compress[k] + + def _set_section(self, section): + """Set the renderer's current section. + + Sections must be rendered order: QUESTION, ANSWER, AUTHORITY, + ADDITIONAL. Sections may be empty. + + Raises dns.exception.FormError if an attempt was made to set + a section value less than the current section. + """ + + if self.section != section: + if self.section > section: + raise dns.exception.FormError + self.section = section + + @contextlib.contextmanager + def _track_size(self): + start = self.output.tell() + yield start + if self.output.tell() > self.max_size: + self._rollback(start) + raise dns.exception.TooBig + + @contextlib.contextmanager + def _temporarily_seek_to(self, where): + current = self.output.tell() + try: + self.output.seek(where) + yield + finally: + self.output.seek(current) + + def add_question(self, qname, rdtype, rdclass=dns.rdataclass.IN): + """Add a question to the message.""" + + self._set_section(QUESTION) + with self._track_size(): + qname.to_wire(self.output, self.compress, self.origin) + self.output.write(struct.pack("!HH", rdtype, rdclass)) + self.counts[QUESTION] += 1 + + def add_rrset(self, section, rrset, **kw): + """Add the rrset to the specified section. + + Any keyword arguments are passed on to the rdataset's to_wire() + routine. + """ + + self._set_section(section) + with self._track_size(): + n = rrset.to_wire(self.output, self.compress, self.origin, **kw) + self.counts[section] += n + + def add_rdataset(self, section, name, rdataset, **kw): + """Add the rdataset to the specified section, using the specified + name as the owner name. + + Any keyword arguments are passed on to the rdataset's to_wire() + routine. + """ + + self._set_section(section) + with self._track_size(): + n = rdataset.to_wire(name, self.output, self.compress, self.origin, **kw) + self.counts[section] += n + + def add_opt(self, opt, pad=0, opt_size=0, tsig_size=0): + """Add *opt* to the additional section, applying padding if desired. The + padding will take the specified precomputed OPT size and TSIG size into + account. + + Note that we don't have reliable way of knowing how big a GSS-TSIG digest + might be, so we we might not get an even multiple of the pad in that case.""" + if pad: + ttl = opt.ttl + assert opt_size >= 11 + opt_rdata = opt[0] + size_without_padding = self.output.tell() + opt_size + tsig_size + remainder = size_without_padding % pad + if remainder: + pad = b"\x00" * (pad - remainder) + else: + pad = b"" + options = list(opt_rdata.options) + options.append(dns.edns.GenericOption(dns.edns.OptionType.PADDING, pad)) + opt = dns.message.Message._make_opt( # pyright: ignore + ttl, opt_rdata.rdclass, options + ) + self.was_padded = True + self.add_rrset(ADDITIONAL, opt) + + def add_edns(self, edns, ednsflags, payload, options=None): + """Add an EDNS OPT record to the message.""" + + # make sure the EDNS version in ednsflags agrees with edns + ednsflags &= 0xFF00FFFF + ednsflags |= edns << 16 + opt = dns.message.Message._make_opt( # pyright: ignore + ednsflags, payload, options + ) + self.add_opt(opt) + + def add_tsig( + self, + keyname, + secret, + fudge, + id, + tsig_error, + other_data, + request_mac, + algorithm=dns.tsig.default_algorithm, + ): + """Add a TSIG signature to the message.""" + + s = self.output.getvalue() + + if isinstance(secret, dns.tsig.Key): + key = secret + else: + key = dns.tsig.Key(keyname, secret, algorithm) + tsig = dns.message.Message._make_tsig( # pyright: ignore + keyname, algorithm, 0, fudge, b"", id, tsig_error, other_data + ) + (tsig, _) = dns.tsig.sign(s, key, tsig[0], int(time.time()), request_mac) + self._write_tsig(tsig, keyname) + + def add_multi_tsig( + self, + ctx, + keyname, + secret, + fudge, + id, + tsig_error, + other_data, + request_mac, + algorithm=dns.tsig.default_algorithm, + ): + """Add a TSIG signature to the message. Unlike add_tsig(), this can be + used for a series of consecutive DNS envelopes, e.g. for a zone + transfer over TCP [RFC2845, 4.4]. + + For the first message in the sequence, give ctx=None. For each + subsequent message, give the ctx that was returned from the + add_multi_tsig() call for the previous message.""" + + s = self.output.getvalue() + + if isinstance(secret, dns.tsig.Key): + key = secret + else: + key = dns.tsig.Key(keyname, secret, algorithm) + tsig = dns.message.Message._make_tsig( # pyright: ignore + keyname, algorithm, 0, fudge, b"", id, tsig_error, other_data + ) + (tsig, ctx) = dns.tsig.sign( + s, key, tsig[0], int(time.time()), request_mac, ctx, True + ) + self._write_tsig(tsig, keyname) + return ctx + + def _write_tsig(self, tsig, keyname): + if self.was_padded: + compress = None + else: + compress = self.compress + self._set_section(ADDITIONAL) + with self._track_size(): + keyname.to_wire(self.output, compress, self.origin) + self.output.write( + struct.pack("!HHI", dns.rdatatype.TSIG, dns.rdataclass.ANY, 0) + ) + with prefixed_length(self.output, 2): + tsig.to_wire(self.output) + + self.counts[ADDITIONAL] += 1 + with self._temporarily_seek_to(10): + self.output.write(struct.pack("!H", self.counts[ADDITIONAL])) + + def write_header(self): + """Write the DNS message header. + + Writing the DNS message header is done after all sections + have been rendered, but before the optional TSIG signature + is added. + """ + + with self._temporarily_seek_to(0): + self.output.write( + struct.pack( + "!HHHHHH", + self.id, + self.flags, + self.counts[0], + self.counts[1], + self.counts[2], + self.counts[3], + ) + ) + + def get_wire(self): + """Return the wire format message.""" + + return self.output.getvalue() + + def reserve(self, size: int) -> None: + """Reserve *size* bytes.""" + if size < 0: + raise ValueError("reserved amount must be non-negative") + if size > self.max_size: + raise ValueError("cannot reserve more than the maximum size") + self.reserved += size + self.max_size -= size + + def release_reserved(self) -> None: + """Release the reserved bytes.""" + self.max_size += self.reserved + self.reserved = 0 diff --git a/netdeploy/lib/python3.11/site-packages/dns/resolver.py b/netdeploy/lib/python3.11/site-packages/dns/resolver.py new file mode 100644 index 0000000..923bb4b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/resolver.py @@ -0,0 +1,2068 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS stub resolver.""" + +import contextlib +import random +import socket +import sys +import threading +import time +import warnings +from typing import Any, Dict, Iterator, List, Sequence, Tuple, cast +from urllib.parse import urlparse + +import dns._ddr +import dns.edns +import dns.exception +import dns.flags +import dns.inet +import dns.ipv4 +import dns.ipv6 +import dns.message +import dns.name +import dns.nameserver +import dns.query +import dns.rcode +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.rdtypes.ANY.PTR +import dns.rdtypes.svcbbase +import dns.reversename +import dns.tsig + +if sys.platform == "win32": # pragma: no cover + import dns.win32util + + +class NXDOMAIN(dns.exception.DNSException): + """The DNS query name does not exist.""" + + supp_kwargs = {"qnames", "responses"} + fmt = None # we have our own __str__ implementation + + # pylint: disable=arguments-differ + + # We do this as otherwise mypy complains about unexpected keyword argument + # idna_exception + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _check_kwargs(self, qnames, responses=None): # pyright: ignore + if not isinstance(qnames, list | tuple | set): + raise AttributeError("qnames must be a list, tuple or set") + if len(qnames) == 0: + raise AttributeError("qnames must contain at least one element") + if responses is None: + responses = {} + elif not isinstance(responses, dict): + raise AttributeError("responses must be a dict(qname=response)") + kwargs = dict(qnames=qnames, responses=responses) + return kwargs + + def __str__(self) -> str: + if "qnames" not in self.kwargs: + return super().__str__() + qnames = self.kwargs["qnames"] + if len(qnames) > 1: + msg = "None of DNS query names exist" + else: + msg = "The DNS query name does not exist" + qnames = ", ".join(map(str, qnames)) + return f"{msg}: {qnames}" + + @property + def canonical_name(self): + """Return the unresolved canonical name.""" + if "qnames" not in self.kwargs: + raise TypeError("parametrized exception required") + for qname in self.kwargs["qnames"]: + response = self.kwargs["responses"][qname] + try: + cname = response.canonical_name() + if cname != qname: + return cname + except Exception: # pragma: no cover + # We can just eat this exception as it means there was + # something wrong with the response. + pass + return self.kwargs["qnames"][0] + + def __add__(self, e_nx): + """Augment by results from another NXDOMAIN exception.""" + qnames0 = list(self.kwargs.get("qnames", [])) + responses0 = dict(self.kwargs.get("responses", {})) + responses1 = e_nx.kwargs.get("responses", {}) + for qname1 in e_nx.kwargs.get("qnames", []): + if qname1 not in qnames0: + qnames0.append(qname1) + if qname1 in responses1: + responses0[qname1] = responses1[qname1] + return NXDOMAIN(qnames=qnames0, responses=responses0) + + def qnames(self): + """All of the names that were tried. + + Returns a list of ``dns.name.Name``. + """ + return self.kwargs["qnames"] + + def responses(self): + """A map from queried names to their NXDOMAIN responses. + + Returns a dict mapping a ``dns.name.Name`` to a + ``dns.message.Message``. + """ + return self.kwargs["responses"] + + def response(self, qname): + """The response for query *qname*. + + Returns a ``dns.message.Message``. + """ + return self.kwargs["responses"][qname] + + +class YXDOMAIN(dns.exception.DNSException): + """The DNS query name is too long after DNAME substitution.""" + + +ErrorTuple = Tuple[ + str | None, + bool, + int, + Exception | str, + dns.message.Message | None, +] + + +def _errors_to_text(errors: List[ErrorTuple]) -> List[str]: + """Turn a resolution errors trace into a list of text.""" + texts = [] + for err in errors: + texts.append(f"Server {err[0]} answered {err[3]}") + return texts + + +class LifetimeTimeout(dns.exception.Timeout): + """The resolution lifetime expired.""" + + msg = "The resolution lifetime expired." + fmt = f"{msg[:-1]} after {{timeout:.3f}} seconds: {{errors}}" + supp_kwargs = {"timeout", "errors"} + + # We do this as otherwise mypy complains about unexpected keyword argument + # idna_exception + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _fmt_kwargs(self, **kwargs): + srv_msgs = _errors_to_text(kwargs["errors"]) + return super()._fmt_kwargs( + timeout=kwargs["timeout"], errors="; ".join(srv_msgs) + ) + + +# We added more detail to resolution timeouts, but they are still +# subclasses of dns.exception.Timeout for backwards compatibility. We also +# keep dns.resolver.Timeout defined for backwards compatibility. +Timeout = LifetimeTimeout + + +class NoAnswer(dns.exception.DNSException): + """The DNS response does not contain an answer to the question.""" + + fmt = "The DNS response does not contain an answer to the question: {query}" + supp_kwargs = {"response"} + + # We do this as otherwise mypy complains about unexpected keyword argument + # idna_exception + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _fmt_kwargs(self, **kwargs): + return super()._fmt_kwargs(query=kwargs["response"].question) + + def response(self): + return self.kwargs["response"] + + +class NoNameservers(dns.exception.DNSException): + """All nameservers failed to answer the query. + + errors: list of servers and respective errors + The type of errors is + [(server IP address, any object convertible to string)]. + Non-empty errors list will add explanatory message () + """ + + msg = "All nameservers failed to answer the query." + fmt = f"{msg[:-1]} {{query}}: {{errors}}" + supp_kwargs = {"request", "errors"} + + # We do this as otherwise mypy complains about unexpected keyword argument + # idna_exception + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def _fmt_kwargs(self, **kwargs): + srv_msgs = _errors_to_text(kwargs["errors"]) + return super()._fmt_kwargs( + query=kwargs["request"].question, errors="; ".join(srv_msgs) + ) + + +class NotAbsolute(dns.exception.DNSException): + """An absolute domain name is required but a relative name was provided.""" + + +class NoRootSOA(dns.exception.DNSException): + """There is no SOA RR at the DNS root name. This should never happen!""" + + +class NoMetaqueries(dns.exception.DNSException): + """DNS metaqueries are not allowed.""" + + +class NoResolverConfiguration(dns.exception.DNSException): + """Resolver configuration could not be read or specified no nameservers.""" + + +class Answer: + """DNS stub resolver answer. + + Instances of this class bundle up the result of a successful DNS + resolution. + + For convenience, the answer object implements much of the sequence + protocol, forwarding to its ``rrset`` attribute. E.g. + ``for a in answer`` is equivalent to ``for a in answer.rrset``. + ``answer[i]`` is equivalent to ``answer.rrset[i]``, and + ``answer[i:j]`` is equivalent to ``answer.rrset[i:j]``. + + Note that CNAMEs or DNAMEs in the response may mean that answer + RRset's name might not be the query name. + """ + + def __init__( + self, + qname: dns.name.Name, + rdtype: dns.rdatatype.RdataType, + rdclass: dns.rdataclass.RdataClass, + response: dns.message.QueryMessage, + nameserver: str | None = None, + port: int | None = None, + ) -> None: + self.qname = qname + self.rdtype = rdtype + self.rdclass = rdclass + self.response = response + self.nameserver = nameserver + self.port = port + self.chaining_result = response.resolve_chaining() + # Copy some attributes out of chaining_result for backwards + # compatibility and convenience. + self.canonical_name = self.chaining_result.canonical_name + self.rrset = self.chaining_result.answer + self.expiration = time.time() + self.chaining_result.minimum_ttl + + def __getattr__(self, attr): # pragma: no cover + if self.rrset is not None: + if attr == "name": + return self.rrset.name + elif attr == "ttl": + return self.rrset.ttl + elif attr == "covers": + return self.rrset.covers + elif attr == "rdclass": + return self.rrset.rdclass + elif attr == "rdtype": + return self.rrset.rdtype + else: + raise AttributeError(attr) + + def __len__(self) -> int: + return self.rrset is not None and len(self.rrset) or 0 + + def __iter__(self) -> Iterator[Any]: + return self.rrset is not None and iter(self.rrset) or iter(tuple()) + + def __getitem__(self, i): + if self.rrset is None: + raise IndexError + return self.rrset[i] + + def __delitem__(self, i): + if self.rrset is None: + raise IndexError + del self.rrset[i] + + +class Answers(dict): + """A dict of DNS stub resolver answers, indexed by type.""" + + +class EmptyHostAnswers(dns.exception.DNSException): + """The HostAnswers has no addresses""" + + +class HostAnswers(Answers): + """A dict of DNS stub resolver answers to a host name lookup, indexed by + type. + """ + + @classmethod + def make( + cls, + v6: Answer | None = None, + v4: Answer | None = None, + add_empty: bool = True, + ) -> "HostAnswers": + answers = HostAnswers() + if v6 is not None and (add_empty or v6.rrset): + answers[dns.rdatatype.AAAA] = v6 + if v4 is not None and (add_empty or v4.rrset): + answers[dns.rdatatype.A] = v4 + return answers + + # Returns pairs of (address, family) from this result, potentially + # filtering by address family. + def addresses_and_families( + self, family: int = socket.AF_UNSPEC + ) -> Iterator[Tuple[str, int]]: + if family == socket.AF_UNSPEC: + yield from self.addresses_and_families(socket.AF_INET6) + yield from self.addresses_and_families(socket.AF_INET) + return + elif family == socket.AF_INET6: + answer = self.get(dns.rdatatype.AAAA) + elif family == socket.AF_INET: + answer = self.get(dns.rdatatype.A) + else: # pragma: no cover + raise NotImplementedError(f"unknown address family {family}") + if answer: + for rdata in answer: + yield (rdata.address, family) + + # Returns addresses from this result, potentially filtering by + # address family. + def addresses(self, family: int = socket.AF_UNSPEC) -> Iterator[str]: + return (pair[0] for pair in self.addresses_and_families(family)) + + # Returns the canonical name from this result. + def canonical_name(self) -> dns.name.Name: + answer = self.get(dns.rdatatype.AAAA, self.get(dns.rdatatype.A)) + if answer is None: + raise EmptyHostAnswers + return answer.canonical_name + + +class CacheStatistics: + """Cache Statistics""" + + def __init__(self, hits: int = 0, misses: int = 0) -> None: + self.hits = hits + self.misses = misses + + def reset(self) -> None: + self.hits = 0 + self.misses = 0 + + def clone(self) -> "CacheStatistics": + return CacheStatistics(self.hits, self.misses) + + +class CacheBase: + def __init__(self) -> None: + self.lock = threading.Lock() + self.statistics = CacheStatistics() + + def reset_statistics(self) -> None: + """Reset all statistics to zero.""" + with self.lock: + self.statistics.reset() + + def hits(self) -> int: + """How many hits has the cache had?""" + with self.lock: + return self.statistics.hits + + def misses(self) -> int: + """How many misses has the cache had?""" + with self.lock: + return self.statistics.misses + + def get_statistics_snapshot(self) -> CacheStatistics: + """Return a consistent snapshot of all the statistics. + + If running with multiple threads, it's better to take a + snapshot than to call statistics methods such as hits() and + misses() individually. + """ + with self.lock: + return self.statistics.clone() + + +CacheKey = Tuple[dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass] + + +class Cache(CacheBase): + """Simple thread-safe DNS answer cache.""" + + def __init__(self, cleaning_interval: float = 300.0) -> None: + """*cleaning_interval*, a ``float`` is the number of seconds between + periodic cleanings. + """ + + super().__init__() + self.data: Dict[CacheKey, Answer] = {} + self.cleaning_interval = cleaning_interval + self.next_cleaning: float = time.time() + self.cleaning_interval + + def _maybe_clean(self) -> None: + """Clean the cache if it's time to do so.""" + + now = time.time() + if self.next_cleaning <= now: + keys_to_delete = [] + for k, v in self.data.items(): + if v.expiration <= now: + keys_to_delete.append(k) + for k in keys_to_delete: + del self.data[k] + now = time.time() + self.next_cleaning = now + self.cleaning_interval + + def get(self, key: CacheKey) -> Answer | None: + """Get the answer associated with *key*. + + Returns None if no answer is cached for the key. + + *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)`` + tuple whose values are the query name, rdtype, and rdclass respectively. + + Returns a ``dns.resolver.Answer`` or ``None``. + """ + + with self.lock: + self._maybe_clean() + v = self.data.get(key) + if v is None or v.expiration <= time.time(): + self.statistics.misses += 1 + return None + self.statistics.hits += 1 + return v + + def put(self, key: CacheKey, value: Answer) -> None: + """Associate key and value in the cache. + + *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)`` + tuple whose values are the query name, rdtype, and rdclass respectively. + + *value*, a ``dns.resolver.Answer``, the answer. + """ + + with self.lock: + self._maybe_clean() + self.data[key] = value + + def flush(self, key: CacheKey | None = None) -> None: + """Flush the cache. + + If *key* is not ``None``, only that item is flushed. Otherwise the entire cache + is flushed. + + *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)`` + tuple whose values are the query name, rdtype, and rdclass respectively. + """ + + with self.lock: + if key is not None: + if key in self.data: + del self.data[key] + else: + self.data = {} + self.next_cleaning = time.time() + self.cleaning_interval + + +class LRUCacheNode: + """LRUCache node.""" + + def __init__(self, key, value): + self.key = key + self.value = value + self.hits = 0 + self.prev = self + self.next = self + + def link_after(self, node: "LRUCacheNode") -> None: + self.prev = node + self.next = node.next + node.next.prev = self + node.next = self + + def unlink(self) -> None: + self.next.prev = self.prev + self.prev.next = self.next + + +class LRUCache(CacheBase): + """Thread-safe, bounded, least-recently-used DNS answer cache. + + This cache is better than the simple cache (above) if you're + running a web crawler or other process that does a lot of + resolutions. The LRUCache has a maximum number of nodes, and when + it is full, the least-recently used node is removed to make space + for a new one. + """ + + def __init__(self, max_size: int = 100000) -> None: + """*max_size*, an ``int``, is the maximum number of nodes to cache; + it must be greater than 0. + """ + + super().__init__() + self.data: Dict[CacheKey, LRUCacheNode] = {} + self.set_max_size(max_size) + self.sentinel: LRUCacheNode = LRUCacheNode(None, None) + self.sentinel.prev = self.sentinel + self.sentinel.next = self.sentinel + + def set_max_size(self, max_size: int) -> None: + if max_size < 1: + max_size = 1 + self.max_size = max_size + + def get(self, key: CacheKey) -> Answer | None: + """Get the answer associated with *key*. + + Returns None if no answer is cached for the key. + + *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)`` + tuple whose values are the query name, rdtype, and rdclass respectively. + + Returns a ``dns.resolver.Answer`` or ``None``. + """ + + with self.lock: + node = self.data.get(key) + if node is None: + self.statistics.misses += 1 + return None + # Unlink because we're either going to move the node to the front + # of the LRU list or we're going to free it. + node.unlink() + if node.value.expiration <= time.time(): + del self.data[node.key] + self.statistics.misses += 1 + return None + node.link_after(self.sentinel) + self.statistics.hits += 1 + node.hits += 1 + return node.value + + def get_hits_for_key(self, key: CacheKey) -> int: + """Return the number of cache hits associated with the specified key.""" + with self.lock: + node = self.data.get(key) + if node is None or node.value.expiration <= time.time(): + return 0 + else: + return node.hits + + def put(self, key: CacheKey, value: Answer) -> None: + """Associate key and value in the cache. + + *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)`` + tuple whose values are the query name, rdtype, and rdclass respectively. + + *value*, a ``dns.resolver.Answer``, the answer. + """ + + with self.lock: + node = self.data.get(key) + if node is not None: + node.unlink() + del self.data[node.key] + while len(self.data) >= self.max_size: + gnode = self.sentinel.prev + gnode.unlink() + del self.data[gnode.key] + node = LRUCacheNode(key, value) + node.link_after(self.sentinel) + self.data[key] = node + + def flush(self, key: CacheKey | None = None) -> None: + """Flush the cache. + + If *key* is not ``None``, only that item is flushed. Otherwise the entire cache + is flushed. + + *key*, a ``(dns.name.Name, dns.rdatatype.RdataType, dns.rdataclass.RdataClass)`` + tuple whose values are the query name, rdtype, and rdclass respectively. + """ + + with self.lock: + if key is not None: + node = self.data.get(key) + if node is not None: + node.unlink() + del self.data[node.key] + else: + gnode = self.sentinel.next + while gnode != self.sentinel: + next = gnode.next + gnode.unlink() + gnode = next + self.data = {} + + +class _Resolution: + """Helper class for dns.resolver.Resolver.resolve(). + + All of the "business logic" of resolution is encapsulated in this + class, allowing us to have multiple resolve() implementations + using different I/O schemes without copying all of the + complicated logic. + + This class is a "friend" to dns.resolver.Resolver and manipulates + resolver data structures directly. + """ + + def __init__( + self, + resolver: "BaseResolver", + qname: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str, + rdclass: dns.rdataclass.RdataClass | str, + tcp: bool, + raise_on_no_answer: bool, + search: bool | None, + ) -> None: + if isinstance(qname, str): + qname = dns.name.from_text(qname, None) + rdtype = dns.rdatatype.RdataType.make(rdtype) + if dns.rdatatype.is_metatype(rdtype): + raise NoMetaqueries + rdclass = dns.rdataclass.RdataClass.make(rdclass) + if dns.rdataclass.is_metaclass(rdclass): + raise NoMetaqueries + self.resolver = resolver + self.qnames_to_try = resolver._get_qnames_to_try(qname, search) + self.qnames = self.qnames_to_try[:] + self.rdtype = rdtype + self.rdclass = rdclass + self.tcp = tcp + self.raise_on_no_answer = raise_on_no_answer + self.nxdomain_responses: Dict[dns.name.Name, dns.message.QueryMessage] = {} + # Initialize other things to help analysis tools + self.qname = dns.name.empty + self.nameservers: List[dns.nameserver.Nameserver] = [] + self.current_nameservers: List[dns.nameserver.Nameserver] = [] + self.errors: List[ErrorTuple] = [] + self.nameserver: dns.nameserver.Nameserver | None = None + self.tcp_attempt = False + self.retry_with_tcp = False + self.request: dns.message.QueryMessage | None = None + self.backoff = 0.0 + + def next_request( + self, + ) -> Tuple[dns.message.QueryMessage | None, Answer | None]: + """Get the next request to send, and check the cache. + + Returns a (request, answer) tuple. At most one of request or + answer will not be None. + """ + + # We return a tuple instead of Union[Message,Answer] as it lets + # the caller avoid isinstance(). + + while len(self.qnames) > 0: + self.qname = self.qnames.pop(0) + + # Do we know the answer? + if self.resolver.cache: + answer = self.resolver.cache.get( + (self.qname, self.rdtype, self.rdclass) + ) + if answer is not None: + if answer.rrset is None and self.raise_on_no_answer: + raise NoAnswer(response=answer.response) + else: + return (None, answer) + answer = self.resolver.cache.get( + (self.qname, dns.rdatatype.ANY, self.rdclass) + ) + if answer is not None and answer.response.rcode() == dns.rcode.NXDOMAIN: + # cached NXDOMAIN; record it and continue to next + # name. + self.nxdomain_responses[self.qname] = answer.response + continue + + # Build the request + request = dns.message.make_query(self.qname, self.rdtype, self.rdclass) + if self.resolver.keyname is not None: + request.use_tsig( + self.resolver.keyring, + self.resolver.keyname, + algorithm=self.resolver.keyalgorithm, + ) + request.use_edns( + self.resolver.edns, + self.resolver.ednsflags, + self.resolver.payload, + options=self.resolver.ednsoptions, + ) + if self.resolver.flags is not None: + request.flags = self.resolver.flags + + self.nameservers = self.resolver._enrich_nameservers( + self.resolver._nameservers, + self.resolver.nameserver_ports, + self.resolver.port, + ) + if self.resolver.rotate: + random.shuffle(self.nameservers) + self.current_nameservers = self.nameservers[:] + self.errors = [] + self.nameserver = None + self.tcp_attempt = False + self.retry_with_tcp = False + self.request = request + self.backoff = 0.10 + + return (request, None) + + # + # We've tried everything and only gotten NXDOMAINs. (We know + # it's only NXDOMAINs as anything else would have returned + # before now.) + # + raise NXDOMAIN(qnames=self.qnames_to_try, responses=self.nxdomain_responses) + + def next_nameserver(self) -> Tuple[dns.nameserver.Nameserver, bool, float]: + if self.retry_with_tcp: + assert self.nameserver is not None + assert not self.nameserver.is_always_max_size() + self.tcp_attempt = True + self.retry_with_tcp = False + return (self.nameserver, True, 0) + + backoff = 0.0 + if not self.current_nameservers: + if len(self.nameservers) == 0: + # Out of things to try! + raise NoNameservers(request=self.request, errors=self.errors) + self.current_nameservers = self.nameservers[:] + backoff = self.backoff + self.backoff = min(self.backoff * 2, 2) + + self.nameserver = self.current_nameservers.pop(0) + self.tcp_attempt = self.tcp or self.nameserver.is_always_max_size() + return (self.nameserver, self.tcp_attempt, backoff) + + def query_result( + self, response: dns.message.Message | None, ex: Exception | None + ) -> Tuple[Answer | None, bool]: + # + # returns an (answer: Answer, end_loop: bool) tuple. + # + assert self.nameserver is not None + if ex: + # Exception during I/O or from_wire() + assert response is None + self.errors.append( + ( + str(self.nameserver), + self.tcp_attempt, + self.nameserver.answer_port(), + ex, + response, + ) + ) + if ( + isinstance(ex, dns.exception.FormError) + or isinstance(ex, EOFError) + or isinstance(ex, OSError) + or isinstance(ex, NotImplementedError) + ): + # This nameserver is no good, take it out of the mix. + self.nameservers.remove(self.nameserver) + elif isinstance(ex, dns.message.Truncated): + if self.tcp_attempt: + # Truncation with TCP is no good! + self.nameservers.remove(self.nameserver) + else: + self.retry_with_tcp = True + return (None, False) + # We got an answer! + assert response is not None + assert isinstance(response, dns.message.QueryMessage) + rcode = response.rcode() + if rcode == dns.rcode.NOERROR: + try: + answer = Answer( + self.qname, + self.rdtype, + self.rdclass, + response, + self.nameserver.answer_nameserver(), + self.nameserver.answer_port(), + ) + except Exception as e: + self.errors.append( + ( + str(self.nameserver), + self.tcp_attempt, + self.nameserver.answer_port(), + e, + response, + ) + ) + # The nameserver is no good, take it out of the mix. + self.nameservers.remove(self.nameserver) + return (None, False) + if self.resolver.cache: + self.resolver.cache.put((self.qname, self.rdtype, self.rdclass), answer) + if answer.rrset is None and self.raise_on_no_answer: + raise NoAnswer(response=answer.response) + return (answer, True) + elif rcode == dns.rcode.NXDOMAIN: + # Further validate the response by making an Answer, even + # if we aren't going to cache it. + try: + answer = Answer( + self.qname, dns.rdatatype.ANY, dns.rdataclass.IN, response + ) + except Exception as e: + self.errors.append( + ( + str(self.nameserver), + self.tcp_attempt, + self.nameserver.answer_port(), + e, + response, + ) + ) + # The nameserver is no good, take it out of the mix. + self.nameservers.remove(self.nameserver) + return (None, False) + self.nxdomain_responses[self.qname] = response + if self.resolver.cache: + self.resolver.cache.put( + (self.qname, dns.rdatatype.ANY, self.rdclass), answer + ) + # Make next_nameserver() return None, so caller breaks its + # inner loop and calls next_request(). + return (None, True) + elif rcode == dns.rcode.YXDOMAIN: + yex = YXDOMAIN() + self.errors.append( + ( + str(self.nameserver), + self.tcp_attempt, + self.nameserver.answer_port(), + yex, + response, + ) + ) + raise yex + else: + # + # We got a response, but we're not happy with the + # rcode in it. + # + if rcode != dns.rcode.SERVFAIL or not self.resolver.retry_servfail: + self.nameservers.remove(self.nameserver) + self.errors.append( + ( + str(self.nameserver), + self.tcp_attempt, + self.nameserver.answer_port(), + dns.rcode.to_text(rcode), + response, + ) + ) + return (None, False) + + +class BaseResolver: + """DNS stub resolver.""" + + # We initialize in reset() + # + # pylint: disable=attribute-defined-outside-init + + domain: dns.name.Name + nameserver_ports: Dict[str, int] + port: int + search: List[dns.name.Name] + use_search_by_default: bool + timeout: float + lifetime: float + keyring: Any | None + keyname: dns.name.Name | str | None + keyalgorithm: dns.name.Name | str + edns: int + ednsflags: int + ednsoptions: List[dns.edns.Option] | None + payload: int + cache: Any + flags: int | None + retry_servfail: bool + rotate: bool + ndots: int | None + _nameservers: Sequence[str | dns.nameserver.Nameserver] + + def __init__( + self, filename: str = "/etc/resolv.conf", configure: bool = True + ) -> None: + """*filename*, a ``str`` or file object, specifying a file + in standard /etc/resolv.conf format. This parameter is meaningful + only when *configure* is true and the platform is POSIX. + + *configure*, a ``bool``. If True (the default), the resolver + instance is configured in the normal fashion for the operating + system the resolver is running on. (I.e. by reading a + /etc/resolv.conf file on POSIX systems and from the registry + on Windows systems.) + """ + + self.reset() + if configure: + if sys.platform == "win32": # pragma: no cover + self.read_registry() + elif filename: + self.read_resolv_conf(filename) + + def reset(self) -> None: + """Reset all resolver configuration to the defaults.""" + + self.domain = dns.name.Name(dns.name.from_text(socket.gethostname())[1:]) + if len(self.domain) == 0: # pragma: no cover + self.domain = dns.name.root + self._nameservers = [] + self.nameserver_ports = {} + self.port = 53 + self.search = [] + self.use_search_by_default = False + self.timeout = 2.0 + self.lifetime = 5.0 + self.keyring = None + self.keyname = None + self.keyalgorithm = dns.tsig.default_algorithm + self.edns = -1 + self.ednsflags = 0 + self.ednsoptions = None + self.payload = 0 + self.cache = None + self.flags = None + self.retry_servfail = False + self.rotate = False + self.ndots = None + + def read_resolv_conf(self, f: Any) -> None: + """Process *f* as a file in the /etc/resolv.conf format. If f is + a ``str``, it is used as the name of the file to open; otherwise it + is treated as the file itself. + + Interprets the following items: + + - nameserver - name server IP address + + - domain - local domain name + + - search - search list for host-name lookup + + - options - supported options are rotate, timeout, edns0, and ndots + + """ + + nameservers = [] + if isinstance(f, str): + try: + cm: contextlib.AbstractContextManager = open(f, encoding="utf-8") + except OSError: + # /etc/resolv.conf doesn't exist, can't be read, etc. + raise NoResolverConfiguration(f"cannot open {f}") + else: + cm = contextlib.nullcontext(f) + with cm as f: + for l in f: + if len(l) == 0 or l[0] == "#" or l[0] == ";": + continue + tokens = l.split() + + # Any line containing less than 2 tokens is malformed + if len(tokens) < 2: + continue + + if tokens[0] == "nameserver": + nameservers.append(tokens[1]) + elif tokens[0] == "domain": + self.domain = dns.name.from_text(tokens[1]) + # domain and search are exclusive + self.search = [] + elif tokens[0] == "search": + # the last search wins + self.search = [] + for suffix in tokens[1:]: + self.search.append(dns.name.from_text(suffix)) + # We don't set domain as it is not used if + # len(self.search) > 0 + elif tokens[0] == "options": + for opt in tokens[1:]: + if opt == "rotate": + self.rotate = True + elif opt == "edns0": + self.use_edns() + elif "timeout" in opt: + try: + self.timeout = int(opt.split(":")[1]) + except (ValueError, IndexError): + pass + elif "ndots" in opt: + try: + self.ndots = int(opt.split(":")[1]) + except (ValueError, IndexError): + pass + if len(nameservers) == 0: + raise NoResolverConfiguration("no nameservers") + # Assigning directly instead of appending means we invoke the + # setter logic, with additonal checking and enrichment. + self.nameservers = nameservers + + def read_registry(self) -> None: # pragma: no cover + """Extract resolver configuration from the Windows registry.""" + try: + info = dns.win32util.get_dns_info() # type: ignore + if info.domain is not None: + self.domain = info.domain + self.nameservers = info.nameservers + self.search = info.search + except AttributeError: + raise NotImplementedError + + def _compute_timeout( + self, + start: float, + lifetime: float | None = None, + errors: List[ErrorTuple] | None = None, + ) -> float: + lifetime = self.lifetime if lifetime is None else lifetime + now = time.time() + duration = now - start + if errors is None: + errors = [] + if duration < 0: + if duration < -1: + # Time going backwards is bad. Just give up. + raise LifetimeTimeout(timeout=duration, errors=errors) + else: + # Time went backwards, but only a little. This can + # happen, e.g. under vmware with older linux kernels. + # Pretend it didn't happen. + duration = 0 + if duration >= lifetime: + raise LifetimeTimeout(timeout=duration, errors=errors) + return min(lifetime - duration, self.timeout) + + def _get_qnames_to_try( + self, qname: dns.name.Name, search: bool | None + ) -> List[dns.name.Name]: + # This is a separate method so we can unit test the search + # rules without requiring the Internet. + if search is None: + search = self.use_search_by_default + qnames_to_try = [] + if qname.is_absolute(): + qnames_to_try.append(qname) + else: + abs_qname = qname.concatenate(dns.name.root) + if search: + if len(self.search) > 0: + # There is a search list, so use it exclusively + search_list = self.search[:] + elif self.domain != dns.name.root and self.domain is not None: + # We have some notion of a domain that isn't the root, so + # use it as the search list. + search_list = [self.domain] + else: + search_list = [] + # Figure out the effective ndots (default is 1) + if self.ndots is None: + ndots = 1 + else: + ndots = self.ndots + for suffix in search_list: + qnames_to_try.append(qname + suffix) + if len(qname) > ndots: + # The name has at least ndots dots, so we should try an + # absolute query first. + qnames_to_try.insert(0, abs_qname) + else: + # The name has less than ndots dots, so we should search + # first, then try the absolute name. + qnames_to_try.append(abs_qname) + else: + qnames_to_try.append(abs_qname) + return qnames_to_try + + def use_tsig( + self, + keyring: Any, + keyname: dns.name.Name | str | None = None, + algorithm: dns.name.Name | str = dns.tsig.default_algorithm, + ) -> None: + """Add a TSIG signature to each query. + + The parameters are passed to ``dns.message.Message.use_tsig()``; + see its documentation for details. + """ + + self.keyring = keyring + self.keyname = keyname + self.keyalgorithm = algorithm + + def use_edns( + self, + edns: int | bool | None = 0, + ednsflags: int = 0, + payload: int = dns.message.DEFAULT_EDNS_PAYLOAD, + options: List[dns.edns.Option] | None = None, + ) -> None: + """Configure EDNS behavior. + + *edns*, an ``int``, is the EDNS level to use. Specifying + ``None``, ``False``, or ``-1`` means "do not use EDNS", and in this case + the other parameters are ignored. Specifying ``True`` is + equivalent to specifying 0, i.e. "use EDNS0". + + *ednsflags*, an ``int``, the EDNS flag values. + + *payload*, an ``int``, is the EDNS sender's payload field, which is the + maximum size of UDP datagram the sender can handle. I.e. how big + a response to this message can be. + + *options*, a list of ``dns.edns.Option`` objects or ``None``, the EDNS + options. + """ + + if edns is None or edns is False: + edns = -1 + elif edns is True: + edns = 0 + self.edns = edns + self.ednsflags = ednsflags + self.payload = payload + self.ednsoptions = options + + def set_flags(self, flags: int) -> None: + """Overrides the default flags with your own. + + *flags*, an ``int``, the message flags to use. + """ + + self.flags = flags + + @classmethod + def _enrich_nameservers( + cls, + nameservers: Sequence[str | dns.nameserver.Nameserver], + nameserver_ports: Dict[str, int], + default_port: int, + ) -> List[dns.nameserver.Nameserver]: + enriched_nameservers = [] + if isinstance(nameservers, list | tuple): + for nameserver in nameservers: + enriched_nameserver: dns.nameserver.Nameserver + if isinstance(nameserver, dns.nameserver.Nameserver): + enriched_nameserver = nameserver + elif dns.inet.is_address(nameserver): + port = nameserver_ports.get(nameserver, default_port) + enriched_nameserver = dns.nameserver.Do53Nameserver( + nameserver, port + ) + else: + try: + if urlparse(nameserver).scheme != "https": + raise NotImplementedError + except Exception: + raise ValueError( + f"nameserver {nameserver} is not a " + "dns.nameserver.Nameserver instance or text form, " + "IP address, nor a valid https URL" + ) + enriched_nameserver = dns.nameserver.DoHNameserver(nameserver) + enriched_nameservers.append(enriched_nameserver) + else: + raise ValueError( + f"nameservers must be a list or tuple (not a {type(nameservers)})" + ) + return enriched_nameservers + + @property + def nameservers( + self, + ) -> Sequence[str | dns.nameserver.Nameserver]: + return self._nameservers + + @nameservers.setter + def nameservers( + self, nameservers: Sequence[str | dns.nameserver.Nameserver] + ) -> None: + """ + *nameservers*, a ``list`` or ``tuple`` of nameservers, where a nameserver is either + a string interpretable as a nameserver, or a ``dns.nameserver.Nameserver`` + instance. + + Raises ``ValueError`` if *nameservers* is not a list of nameservers. + """ + # We just call _enrich_nameservers() for checking + self._enrich_nameservers(nameservers, self.nameserver_ports, self.port) + self._nameservers = nameservers + + +class Resolver(BaseResolver): + """DNS stub resolver.""" + + def resolve( + self, + qname: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A, + rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + tcp: bool = False, + source: str | None = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: float | None = None, + search: bool | None = None, + ) -> Answer: # pylint: disable=arguments-differ + """Query nameservers to find the answer to the question. + + The *qname*, *rdtype*, and *rdclass* parameters may be objects + of the appropriate type, or strings that can be converted into objects + of the appropriate type. + + *qname*, a ``dns.name.Name`` or ``str``, the query name. + + *rdtype*, an ``int`` or ``str``, the query type. + + *rdclass*, an ``int`` or ``str``, the query class. + + *tcp*, a ``bool``. If ``True``, use TCP to make the query. + + *source*, a ``str`` or ``None``. If not ``None``, bind to this IP + address when making queries. + + *raise_on_no_answer*, a ``bool``. If ``True``, raise + ``dns.resolver.NoAnswer`` if there's no answer to the question. + + *source_port*, an ``int``, the port from which to send the message. + + *lifetime*, a ``float``, how many seconds a query should run + before timing out. + + *search*, a ``bool`` or ``None``, determines whether the + search list configured in the system's resolver configuration + are used for relative names, and whether the resolver's domain + may be added to relative names. The default is ``None``, + which causes the value of the resolver's + ``use_search_by_default`` attribute to be used. + + Raises ``dns.resolver.LifetimeTimeout`` if no answers could be found + in the specified lifetime. + + Raises ``dns.resolver.NXDOMAIN`` if the query name does not exist. + + Raises ``dns.resolver.YXDOMAIN`` if the query name is too long after + DNAME substitution. + + Raises ``dns.resolver.NoAnswer`` if *raise_on_no_answer* is + ``True`` and the query name exists but has no RRset of the + desired type and class. + + Raises ``dns.resolver.NoNameservers`` if no non-broken + nameservers are available to answer the question. + + Returns a ``dns.resolver.Answer`` instance. + + """ + + resolution = _Resolution( + self, qname, rdtype, rdclass, tcp, raise_on_no_answer, search + ) + start = time.time() + while True: + (request, answer) = resolution.next_request() + # Note we need to say "if answer is not None" and not just + # "if answer" because answer implements __len__, and python + # will call that. We want to return if we have an answer + # object, including in cases where its length is 0. + if answer is not None: + # cache hit! + return answer + assert request is not None # needed for type checking + done = False + while not done: + (nameserver, tcp, backoff) = resolution.next_nameserver() + if backoff: + time.sleep(backoff) + timeout = self._compute_timeout(start, lifetime, resolution.errors) + try: + response = nameserver.query( + request, + timeout=timeout, + source=source, + source_port=source_port, + max_size=tcp, + ) + except Exception as ex: + (_, done) = resolution.query_result(None, ex) + continue + (answer, done) = resolution.query_result(response, None) + # Note we need to say "if answer is not None" and not just + # "if answer" because answer implements __len__, and python + # will call that. We want to return if we have an answer + # object, including in cases where its length is 0. + if answer is not None: + return answer + + def query( + self, + qname: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A, + rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + tcp: bool = False, + source: str | None = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: float | None = None, + ) -> Answer: # pragma: no cover + """Query nameservers to find the answer to the question. + + This method calls resolve() with ``search=True``, and is + provided for backwards compatibility with prior versions of + dnspython. See the documentation for the resolve() method for + further details. + """ + warnings.warn( + "please use dns.resolver.Resolver.resolve() instead", + DeprecationWarning, + stacklevel=2, + ) + return self.resolve( + qname, + rdtype, + rdclass, + tcp, + source, + raise_on_no_answer, + source_port, + lifetime, + True, + ) + + def resolve_address(self, ipaddr: str, *args: Any, **kwargs: Any) -> Answer: + """Use a resolver to run a reverse query for PTR records. + + This utilizes the resolve() method to perform a PTR lookup on the + specified IP address. + + *ipaddr*, a ``str``, the IPv4 or IPv6 address you want to get + the PTR record for. + + All other arguments that can be passed to the resolve() function + except for rdtype and rdclass are also supported by this + function. + """ + # We make a modified kwargs for type checking happiness, as otherwise + # we get a legit warning about possibly having rdtype and rdclass + # in the kwargs more than once. + modified_kwargs: Dict[str, Any] = {} + modified_kwargs.update(kwargs) + modified_kwargs["rdtype"] = dns.rdatatype.PTR + modified_kwargs["rdclass"] = dns.rdataclass.IN + return self.resolve( + dns.reversename.from_address(ipaddr), *args, **modified_kwargs + ) + + def resolve_name( + self, + name: dns.name.Name | str, + family: int = socket.AF_UNSPEC, + **kwargs: Any, + ) -> HostAnswers: + """Use a resolver to query for address records. + + This utilizes the resolve() method to perform A and/or AAAA lookups on + the specified name. + + *qname*, a ``dns.name.Name`` or ``str``, the name to resolve. + + *family*, an ``int``, the address family. If socket.AF_UNSPEC + (the default), both A and AAAA records will be retrieved. + + All other arguments that can be passed to the resolve() function + except for rdtype and rdclass are also supported by this + function. + """ + # We make a modified kwargs for type checking happiness, as otherwise + # we get a legit warning about possibly having rdtype and rdclass + # in the kwargs more than once. + modified_kwargs: Dict[str, Any] = {} + modified_kwargs.update(kwargs) + modified_kwargs.pop("rdtype", None) + modified_kwargs["rdclass"] = dns.rdataclass.IN + + if family == socket.AF_INET: + v4 = self.resolve(name, dns.rdatatype.A, **modified_kwargs) + return HostAnswers.make(v4=v4) + elif family == socket.AF_INET6: + v6 = self.resolve(name, dns.rdatatype.AAAA, **modified_kwargs) + return HostAnswers.make(v6=v6) + elif family != socket.AF_UNSPEC: # pragma: no cover + raise NotImplementedError(f"unknown address family {family}") + + raise_on_no_answer = modified_kwargs.pop("raise_on_no_answer", True) + lifetime = modified_kwargs.pop("lifetime", None) + start = time.time() + v6 = self.resolve( + name, + dns.rdatatype.AAAA, + raise_on_no_answer=False, + lifetime=self._compute_timeout(start, lifetime), + **modified_kwargs, + ) + # Note that setting name ensures we query the same name + # for A as we did for AAAA. (This is just in case search lists + # are active by default in the resolver configuration and + # we might be talking to a server that says NXDOMAIN when it + # wants to say NOERROR no data. + name = v6.qname + v4 = self.resolve( + name, + dns.rdatatype.A, + raise_on_no_answer=False, + lifetime=self._compute_timeout(start, lifetime), + **modified_kwargs, + ) + answers = HostAnswers.make(v6=v6, v4=v4, add_empty=not raise_on_no_answer) + if not answers: + raise NoAnswer(response=v6.response) + return answers + + # pylint: disable=redefined-outer-name + + def canonical_name(self, name: dns.name.Name | str) -> dns.name.Name: + """Determine the canonical name of *name*. + + The canonical name is the name the resolver uses for queries + after all CNAME and DNAME renamings have been applied. + + *name*, a ``dns.name.Name`` or ``str``, the query name. + + This method can raise any exception that ``resolve()`` can + raise, other than ``dns.resolver.NoAnswer`` and + ``dns.resolver.NXDOMAIN``. + + Returns a ``dns.name.Name``. + """ + try: + answer = self.resolve(name, raise_on_no_answer=False) + canonical_name = answer.canonical_name + except NXDOMAIN as e: + canonical_name = e.canonical_name + return canonical_name + + # pylint: enable=redefined-outer-name + + def try_ddr(self, lifetime: float = 5.0) -> None: + """Try to update the resolver's nameservers using Discovery of Designated + Resolvers (DDR). If successful, the resolver will subsequently use + DNS-over-HTTPS or DNS-over-TLS for future queries. + + *lifetime*, a float, is the maximum time to spend attempting DDR. The default + is 5 seconds. + + If the SVCB query is successful and results in a non-empty list of nameservers, + then the resolver's nameservers are set to the returned servers in priority + order. + + The current implementation does not use any address hints from the SVCB record, + nor does it resolve addresses for the SCVB target name, rather it assumes that + the bootstrap nameserver will always be one of the addresses and uses it. + A future revision to the code may offer fuller support. The code verifies that + the bootstrap nameserver is in the Subject Alternative Name field of the + TLS certficate. + """ + try: + expiration = time.time() + lifetime + answer = self.resolve( + dns._ddr._local_resolver_name, "SVCB", lifetime=lifetime + ) + timeout = dns.query._remaining(expiration) + nameservers = dns._ddr._get_nameservers_sync(answer, timeout) + if len(nameservers) > 0: + self.nameservers = nameservers + except Exception: # pragma: no cover + pass + + +#: The default resolver. +default_resolver: Resolver | None = None + + +def get_default_resolver() -> Resolver: + """Get the default resolver, initializing it if necessary.""" + if default_resolver is None: + reset_default_resolver() + assert default_resolver is not None + return default_resolver + + +def reset_default_resolver() -> None: + """Re-initialize default resolver. + + Note that the resolver configuration (i.e. /etc/resolv.conf on UNIX + systems) will be re-read immediately. + """ + + global default_resolver + default_resolver = Resolver() + + +def resolve( + qname: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A, + rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + tcp: bool = False, + source: str | None = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: float | None = None, + search: bool | None = None, +) -> Answer: # pragma: no cover + """Query nameservers to find the answer to the question. + + This is a convenience function that uses the default resolver + object to make the query. + + See ``dns.resolver.Resolver.resolve`` for more information on the + parameters. + """ + + return get_default_resolver().resolve( + qname, + rdtype, + rdclass, + tcp, + source, + raise_on_no_answer, + source_port, + lifetime, + search, + ) + + +def query( + qname: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A, + rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + tcp: bool = False, + source: str | None = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: float | None = None, +) -> Answer: # pragma: no cover + """Query nameservers to find the answer to the question. + + This method calls resolve() with ``search=True``, and is + provided for backwards compatibility with prior versions of + dnspython. See the documentation for the resolve() method for + further details. + """ + warnings.warn( + "please use dns.resolver.resolve() instead", DeprecationWarning, stacklevel=2 + ) + return resolve( + qname, + rdtype, + rdclass, + tcp, + source, + raise_on_no_answer, + source_port, + lifetime, + True, + ) + + +def resolve_address(ipaddr: str, *args: Any, **kwargs: Any) -> Answer: + """Use a resolver to run a reverse query for PTR records. + + See ``dns.resolver.Resolver.resolve_address`` for more information on the + parameters. + """ + + return get_default_resolver().resolve_address(ipaddr, *args, **kwargs) + + +def resolve_name( + name: dns.name.Name | str, family: int = socket.AF_UNSPEC, **kwargs: Any +) -> HostAnswers: + """Use a resolver to query for address records. + + See ``dns.resolver.Resolver.resolve_name`` for more information on the + parameters. + """ + + return get_default_resolver().resolve_name(name, family, **kwargs) + + +def canonical_name(name: dns.name.Name | str) -> dns.name.Name: + """Determine the canonical name of *name*. + + See ``dns.resolver.Resolver.canonical_name`` for more information on the + parameters and possible exceptions. + """ + + return get_default_resolver().canonical_name(name) + + +def try_ddr(lifetime: float = 5.0) -> None: # pragma: no cover + """Try to update the default resolver's nameservers using Discovery of Designated + Resolvers (DDR). If successful, the resolver will subsequently use + DNS-over-HTTPS or DNS-over-TLS for future queries. + + See :py:func:`dns.resolver.Resolver.try_ddr` for more information. + """ + return get_default_resolver().try_ddr(lifetime) + + +def zone_for_name( + name: dns.name.Name | str, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + tcp: bool = False, + resolver: Resolver | None = None, + lifetime: float | None = None, +) -> dns.name.Name: # pyright: ignore[reportReturnType] + """Find the name of the zone which contains the specified name. + + *name*, an absolute ``dns.name.Name`` or ``str``, the query name. + + *rdclass*, an ``int``, the query class. + + *tcp*, a ``bool``. If ``True``, use TCP to make the query. + + *resolver*, a ``dns.resolver.Resolver`` or ``None``, the resolver to use. + If ``None``, the default, then the default resolver is used. + + *lifetime*, a ``float``, the total time to allow for the queries needed + to determine the zone. If ``None``, the default, then only the individual + query limits of the resolver apply. + + Raises ``dns.resolver.NoRootSOA`` if there is no SOA RR at the DNS + root. (This is only likely to happen if you're using non-default + root servers in your network and they are misconfigured.) + + Raises ``dns.resolver.LifetimeTimeout`` if the answer could not be + found in the allotted lifetime. + + Returns a ``dns.name.Name``. + """ + + if isinstance(name, str): + name = dns.name.from_text(name, dns.name.root) + if resolver is None: + resolver = get_default_resolver() + if not name.is_absolute(): + raise NotAbsolute(name) + start = time.time() + expiration: float | None + if lifetime is not None: + expiration = start + lifetime + else: + expiration = None + while 1: + try: + rlifetime: float | None + if expiration is not None: + rlifetime = expiration - time.time() + if rlifetime <= 0: + rlifetime = 0 + else: + rlifetime = None + answer = resolver.resolve( + name, dns.rdatatype.SOA, rdclass, tcp, lifetime=rlifetime + ) + assert answer.rrset is not None + if answer.rrset.name == name: + return name + # otherwise we were CNAMEd or DNAMEd and need to look higher + except (NXDOMAIN, NoAnswer) as e: + if isinstance(e, NXDOMAIN): + response = e.responses().get(name) + else: + response = e.response() # pylint: disable=no-value-for-parameter + if response: + for rrs in response.authority: + if rrs.rdtype == dns.rdatatype.SOA and rrs.rdclass == rdclass: + (nr, _, _) = rrs.name.fullcompare(name) + if nr == dns.name.NAMERELN_SUPERDOMAIN: + # We're doing a proper superdomain check as + # if the name were equal we ought to have gotten + # it in the answer section! We are ignoring the + # possibility that the authority is insane and + # is including multiple SOA RRs for different + # authorities. + return rrs.name + # we couldn't extract anything useful from the response (e.g. it's + # a type 3 NXDOMAIN) + try: + name = name.parent() + except dns.name.NoParent: + raise NoRootSOA + + +def make_resolver_at( + where: dns.name.Name | str, + port: int = 53, + family: int = socket.AF_UNSPEC, + resolver: Resolver | None = None, +) -> Resolver: + """Make a stub resolver using the specified destination as the full resolver. + + *where*, a ``dns.name.Name`` or ``str`` the domain name or IP address of the + full resolver. + + *port*, an ``int``, the port to use. If not specified, the default is 53. + + *family*, an ``int``, the address family to use. This parameter is used if + *where* is not an address. The default is ``socket.AF_UNSPEC`` in which case + the first address returned by ``resolve_name()`` will be used, otherwise the + first address of the specified family will be used. + + *resolver*, a ``dns.resolver.Resolver`` or ``None``, the resolver to use for + resolution of hostnames. If not specified, the default resolver will be used. + + Returns a ``dns.resolver.Resolver`` or raises an exception. + """ + if resolver is None: + resolver = get_default_resolver() + nameservers: List[str | dns.nameserver.Nameserver] = [] + if isinstance(where, str) and dns.inet.is_address(where): + nameservers.append(dns.nameserver.Do53Nameserver(where, port)) + else: + for address in resolver.resolve_name(where, family).addresses(): + nameservers.append(dns.nameserver.Do53Nameserver(address, port)) + res = Resolver(configure=False) + res.nameservers = nameservers + return res + + +def resolve_at( + where: dns.name.Name | str, + qname: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.A, + rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + tcp: bool = False, + source: str | None = None, + raise_on_no_answer: bool = True, + source_port: int = 0, + lifetime: float | None = None, + search: bool | None = None, + port: int = 53, + family: int = socket.AF_UNSPEC, + resolver: Resolver | None = None, +) -> Answer: + """Query nameservers to find the answer to the question. + + This is a convenience function that calls ``dns.resolver.make_resolver_at()`` to + make a resolver, and then uses it to resolve the query. + + See ``dns.resolver.Resolver.resolve`` for more information on the resolution + parameters, and ``dns.resolver.make_resolver_at`` for information about the resolver + parameters *where*, *port*, *family*, and *resolver*. + + If making more than one query, it is more efficient to call + ``dns.resolver.make_resolver_at()`` and then use that resolver for the queries + instead of calling ``resolve_at()`` multiple times. + """ + return make_resolver_at(where, port, family, resolver).resolve( + qname, + rdtype, + rdclass, + tcp, + source, + raise_on_no_answer, + source_port, + lifetime, + search, + ) + + +# +# Support for overriding the system resolver for all python code in the +# running process. +# + +_protocols_for_socktype: Dict[Any, List[Any]] = { + socket.SOCK_DGRAM: [socket.SOL_UDP], + socket.SOCK_STREAM: [socket.SOL_TCP], +} + +_resolver: Resolver | None = None +_original_getaddrinfo = socket.getaddrinfo +_original_getnameinfo = socket.getnameinfo +_original_getfqdn = socket.getfqdn +_original_gethostbyname = socket.gethostbyname +_original_gethostbyname_ex = socket.gethostbyname_ex +_original_gethostbyaddr = socket.gethostbyaddr + + +def _getaddrinfo( + host=None, service=None, family=socket.AF_UNSPEC, socktype=0, proto=0, flags=0 +): + if flags & socket.AI_NUMERICHOST != 0: + # Short circuit directly into the system's getaddrinfo(). We're + # not adding any value in this case, and this avoids infinite loops + # because dns.query.* needs to call getaddrinfo() for IPv6 scoping + # reasons. We will also do this short circuit below if we + # discover that the host is an address literal. + return _original_getaddrinfo(host, service, family, socktype, proto, flags) + if flags & (socket.AI_ADDRCONFIG | socket.AI_V4MAPPED) != 0: + # Not implemented. We raise a gaierror as opposed to a + # NotImplementedError as it helps callers handle errors more + # appropriately. [Issue #316] + # + # We raise EAI_FAIL as opposed to EAI_SYSTEM because there is + # no EAI_SYSTEM on Windows [Issue #416]. We didn't go for + # EAI_BADFLAGS as the flags aren't bad, we just don't + # implement them. + raise socket.gaierror( + socket.EAI_FAIL, "Non-recoverable failure in name resolution" + ) + if host is None and service is None: + raise socket.gaierror(socket.EAI_NONAME, "Name or service not known") + addrs = [] + canonical_name = None # pylint: disable=redefined-outer-name + # Is host None or an address literal? If so, use the system's + # getaddrinfo(). + if host is None: + return _original_getaddrinfo(host, service, family, socktype, proto, flags) + try: + # We don't care about the result of af_for_address(), we're just + # calling it so it raises an exception if host is not an IPv4 or + # IPv6 address. + dns.inet.af_for_address(host) + return _original_getaddrinfo(host, service, family, socktype, proto, flags) + except Exception: + pass + # Something needs resolution! + try: + assert _resolver is not None + answers = _resolver.resolve_name(host, family) + addrs = answers.addresses_and_families() + canonical_name = answers.canonical_name().to_text(True) + except NXDOMAIN: + raise socket.gaierror(socket.EAI_NONAME, "Name or service not known") + except Exception: + # We raise EAI_AGAIN here as the failure may be temporary + # (e.g. a timeout) and EAI_SYSTEM isn't defined on Windows. + # [Issue #416] + raise socket.gaierror(socket.EAI_AGAIN, "Temporary failure in name resolution") + port = None + try: + # Is it a port literal? + if service is None: + port = 0 + else: + port = int(service) + except Exception: + if flags & socket.AI_NUMERICSERV == 0: + try: + port = socket.getservbyname(service) # pyright: ignore + except Exception: + pass + if port is None: + raise socket.gaierror(socket.EAI_NONAME, "Name or service not known") + tuples = [] + if socktype == 0: + socktypes = [socket.SOCK_DGRAM, socket.SOCK_STREAM] + else: + socktypes = [socktype] + if flags & socket.AI_CANONNAME != 0: + cname = canonical_name + else: + cname = "" + for addr, af in addrs: + for socktype in socktypes: + for sockproto in _protocols_for_socktype[socktype]: + proto = int(sockproto) + addr_tuple = dns.inet.low_level_address_tuple((addr, port), af) + tuples.append((af, socktype, proto, cname, addr_tuple)) + if len(tuples) == 0: + raise socket.gaierror(socket.EAI_NONAME, "Name or service not known") + return tuples + + +def _getnameinfo(sockaddr, flags=0): + host = sockaddr[0] + port = sockaddr[1] + if len(sockaddr) == 4: + scope = sockaddr[3] + family = socket.AF_INET6 + else: + scope = None + family = socket.AF_INET + tuples = _getaddrinfo(host, port, family, socket.SOCK_STREAM, socket.SOL_TCP, 0) + if len(tuples) > 1: + raise OSError("sockaddr resolved to multiple addresses") + addr = tuples[0][4][0] + if flags & socket.NI_DGRAM: + pname = "udp" + else: + pname = "tcp" + assert isinstance(addr, str) + qname = dns.reversename.from_address(addr) + if flags & socket.NI_NUMERICHOST == 0: + try: + assert _resolver is not None + answer = _resolver.resolve(qname, "PTR") + assert answer.rrset is not None + rdata = cast(dns.rdtypes.ANY.PTR.PTR, answer.rrset[0]) + hostname = rdata.target.to_text(True) + except (NXDOMAIN, NoAnswer): + if flags & socket.NI_NAMEREQD: + raise socket.gaierror(socket.EAI_NONAME, "Name or service not known") + hostname = addr + if scope is not None: + hostname += "%" + str(scope) + else: + hostname = addr + if scope is not None: + hostname += "%" + str(scope) + if flags & socket.NI_NUMERICSERV: + service = str(port) + else: + service = socket.getservbyport(port, pname) + return (hostname, service) + + +def _getfqdn(name=None): + if name is None: + name = socket.gethostname() + try: + (name, _, _) = _gethostbyaddr(name) + # Python's version checks aliases too, but our gethostbyname + # ignores them, so we do so here as well. + except Exception: # pragma: no cover + pass + return name + + +def _gethostbyname(name): + return _gethostbyname_ex(name)[2][0] + + +def _gethostbyname_ex(name): + aliases = [] + addresses = [] + tuples = _getaddrinfo( + name, 0, socket.AF_INET, socket.SOCK_STREAM, socket.SOL_TCP, socket.AI_CANONNAME + ) + canonical = tuples[0][3] + for item in tuples: + addresses.append(item[4][0]) + # XXX we just ignore aliases + return (canonical, aliases, addresses) + + +def _gethostbyaddr(ip): + try: + dns.ipv6.inet_aton(ip) + sockaddr = (ip, 80, 0, 0) + family = socket.AF_INET6 + except Exception: + try: + dns.ipv4.inet_aton(ip) + except Exception: + raise socket.gaierror(socket.EAI_NONAME, "Name or service not known") + sockaddr = (ip, 80) + family = socket.AF_INET + (name, _) = _getnameinfo(sockaddr, socket.NI_NAMEREQD) + aliases = [] + addresses = [] + tuples = _getaddrinfo( + name, 0, family, socket.SOCK_STREAM, socket.SOL_TCP, socket.AI_CANONNAME + ) + canonical = tuples[0][3] + # We only want to include an address from the tuples if it's the + # same as the one we asked about. We do this comparison in binary + # to avoid any differences in text representations. + bin_ip = dns.inet.inet_pton(family, ip) + for item in tuples: + addr = item[4][0] + assert isinstance(addr, str) + bin_addr = dns.inet.inet_pton(family, addr) + if bin_ip == bin_addr: + addresses.append(addr) + # XXX we just ignore aliases + return (canonical, aliases, addresses) + + +def override_system_resolver(resolver: Resolver | None = None) -> None: + """Override the system resolver routines in the socket module with + versions which use dnspython's resolver. + + This can be useful in testing situations where you want to control + the resolution behavior of python code without having to change + the system's resolver settings (e.g. /etc/resolv.conf). + + The resolver to use may be specified; if it's not, the default + resolver will be used. + + resolver, a ``dns.resolver.Resolver`` or ``None``, the resolver to use. + """ + + if resolver is None: + resolver = get_default_resolver() + global _resolver + _resolver = resolver + socket.getaddrinfo = _getaddrinfo + socket.getnameinfo = _getnameinfo + socket.getfqdn = _getfqdn + socket.gethostbyname = _gethostbyname + socket.gethostbyname_ex = _gethostbyname_ex + socket.gethostbyaddr = _gethostbyaddr + + +def restore_system_resolver() -> None: + """Undo the effects of prior override_system_resolver().""" + + global _resolver + _resolver = None + socket.getaddrinfo = _original_getaddrinfo + socket.getnameinfo = _original_getnameinfo + socket.getfqdn = _original_getfqdn + socket.gethostbyname = _original_gethostbyname + socket.gethostbyname_ex = _original_gethostbyname_ex + socket.gethostbyaddr = _original_gethostbyaddr diff --git a/netdeploy/lib/python3.11/site-packages/dns/reversename.py b/netdeploy/lib/python3.11/site-packages/dns/reversename.py new file mode 100644 index 0000000..60a4e83 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/reversename.py @@ -0,0 +1,106 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2006-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Reverse Map Names.""" + +import binascii + +import dns.exception +import dns.ipv4 +import dns.ipv6 +import dns.name + +ipv4_reverse_domain = dns.name.from_text("in-addr.arpa.") +ipv6_reverse_domain = dns.name.from_text("ip6.arpa.") + + +def from_address( + text: str, + v4_origin: dns.name.Name = ipv4_reverse_domain, + v6_origin: dns.name.Name = ipv6_reverse_domain, +) -> dns.name.Name: + """Convert an IPv4 or IPv6 address in textual form into a Name object whose + value is the reverse-map domain name of the address. + + *text*, a ``str``, is an IPv4 or IPv6 address in textual form + (e.g. '127.0.0.1', '::1') + + *v4_origin*, a ``dns.name.Name`` to append to the labels corresponding to + the address if the address is an IPv4 address, instead of the default + (in-addr.arpa.) + + *v6_origin*, a ``dns.name.Name`` to append to the labels corresponding to + the address if the address is an IPv6 address, instead of the default + (ip6.arpa.) + + Raises ``dns.exception.SyntaxError`` if the address is badly formed. + + Returns a ``dns.name.Name``. + """ + + try: + v6 = dns.ipv6.inet_aton(text) + if dns.ipv6.is_mapped(v6): + parts = [str(byte) for byte in v6[12:]] + origin = v4_origin + else: + parts = [x for x in str(binascii.hexlify(v6).decode())] + origin = v6_origin + except Exception: + parts = [str(byte) for byte in dns.ipv4.inet_aton(text)] + origin = v4_origin + return dns.name.from_text(".".join(reversed(parts)), origin=origin) + + +def to_address( + name: dns.name.Name, + v4_origin: dns.name.Name = ipv4_reverse_domain, + v6_origin: dns.name.Name = ipv6_reverse_domain, +) -> str: + """Convert a reverse map domain name into textual address form. + + *name*, a ``dns.name.Name``, an IPv4 or IPv6 address in reverse-map name + form. + + *v4_origin*, a ``dns.name.Name`` representing the top-level domain for + IPv4 addresses, instead of the default (in-addr.arpa.) + + *v6_origin*, a ``dns.name.Name`` representing the top-level domain for + IPv4 addresses, instead of the default (ip6.arpa.) + + Raises ``dns.exception.SyntaxError`` if the name does not have a + reverse-map form. + + Returns a ``str``. + """ + + if name.is_subdomain(v4_origin): + name = name.relativize(v4_origin) + text = b".".join(reversed(name.labels)) + # run through inet_ntoa() to check syntax and make pretty. + return dns.ipv4.inet_ntoa(dns.ipv4.inet_aton(text)) + elif name.is_subdomain(v6_origin): + name = name.relativize(v6_origin) + labels = list(reversed(name.labels)) + parts = [] + for i in range(0, len(labels), 4): + parts.append(b"".join(labels[i : i + 4])) + text = b":".join(parts) + # run through inet_ntoa() to check syntax and make pretty. + return dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(text)) + else: + raise dns.exception.SyntaxError("unknown reverse-map address family") diff --git a/netdeploy/lib/python3.11/site-packages/dns/rrset.py b/netdeploy/lib/python3.11/site-packages/dns/rrset.py new file mode 100644 index 0000000..271ddbe --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/rrset.py @@ -0,0 +1,287 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS RRsets (an RRset is a named rdataset)""" + +from typing import Any, Collection, Dict, cast + +import dns.name +import dns.rdata +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.renderer + + +class RRset(dns.rdataset.Rdataset): + """A DNS RRset (named rdataset). + + RRset inherits from Rdataset, and RRsets can be treated as + Rdatasets in most cases. There are, however, a few notable + exceptions. RRsets have different to_wire() and to_text() method + arguments, reflecting the fact that RRsets always have an owner + name. + """ + + __slots__ = ["name", "deleting"] + + def __init__( + self, + name: dns.name.Name, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + deleting: dns.rdataclass.RdataClass | None = None, + ): + """Create a new RRset.""" + + super().__init__(rdclass, rdtype, covers) + self.name = name + self.deleting = deleting + + def _clone(self): + obj = cast(RRset, super()._clone()) + obj.name = self.name + obj.deleting = self.deleting + return obj + + def __repr__(self): + if self.covers == 0: + ctext = "" + else: + ctext = "(" + dns.rdatatype.to_text(self.covers) + ")" + if self.deleting is not None: + dtext = " delete=" + dns.rdataclass.to_text(self.deleting) + else: + dtext = "" + return ( + "" + ) + + def __str__(self): + return self.to_text() + + def __eq__(self, other): + if isinstance(other, RRset): + if self.name != other.name: + return False + elif not isinstance(other, dns.rdataset.Rdataset): + return False + return super().__eq__(other) + + def match(self, *args: Any, **kwargs: Any) -> bool: # type: ignore[override] + """Does this rrset match the specified attributes? + + Behaves as :py:func:`full_match()` if the first argument is a + ``dns.name.Name``, and as :py:func:`dns.rdataset.Rdataset.match()` + otherwise. + + (This behavior fixes a design mistake where the signature of this + method became incompatible with that of its superclass. The fix + makes RRsets matchable as Rdatasets while preserving backwards + compatibility.) + """ + if isinstance(args[0], dns.name.Name): + return self.full_match(*args, **kwargs) # type: ignore[arg-type] + else: + return super().match(*args, **kwargs) # type: ignore[arg-type] + + def full_match( + self, + name: dns.name.Name, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType, + deleting: dns.rdataclass.RdataClass | None = None, + ) -> bool: + """Returns ``True`` if this rrset matches the specified name, class, + type, covers, and deletion state. + """ + if not super().match(rdclass, rdtype, covers): + return False + if self.name != name or self.deleting != deleting: + return False + return True + + # pylint: disable=arguments-differ + + def to_text( # type: ignore[override] + self, + origin: dns.name.Name | None = None, + relativize: bool = True, + **kw: Dict[str, Any], + ) -> str: + """Convert the RRset into DNS zone file format. + + See ``dns.name.Name.choose_relativity`` for more information + on how *origin* and *relativize* determine the way names + are emitted. + + Any additional keyword arguments are passed on to the rdata + ``to_text()`` method. + + *origin*, a ``dns.name.Name`` or ``None``, the origin for relative + names. + + *relativize*, a ``bool``. If ``True``, names will be relativized + to *origin*. + """ + + return super().to_text( + self.name, origin, relativize, self.deleting, **kw # type: ignore + ) + + def to_wire( # type: ignore[override] + self, + file: Any, + compress: dns.name.CompressType | None = None, # type: ignore + origin: dns.name.Name | None = None, + **kw: Dict[str, Any], + ) -> int: + """Convert the RRset to wire format. + + All keyword arguments are passed to ``dns.rdataset.to_wire()``; see + that function for details. + + Returns an ``int``, the number of records emitted. + """ + + return super().to_wire( + self.name, file, compress, origin, self.deleting, **kw # type:ignore + ) + + # pylint: enable=arguments-differ + + def to_rdataset(self) -> dns.rdataset.Rdataset: + """Convert an RRset into an Rdataset. + + Returns a ``dns.rdataset.Rdataset``. + """ + return dns.rdataset.from_rdata_list(self.ttl, list(self)) + + +def from_text_list( + name: dns.name.Name | str, + ttl: int, + rdclass: dns.rdataclass.RdataClass | str, + rdtype: dns.rdatatype.RdataType | str, + text_rdatas: Collection[str], + idna_codec: dns.name.IDNACodec | None = None, + origin: dns.name.Name | None = None, + relativize: bool = True, + relativize_to: dns.name.Name | None = None, +) -> RRset: + """Create an RRset with the specified name, TTL, class, and type, and with + the specified list of rdatas in text format. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder to use; if ``None``, the default IDNA 2003 + encoder/decoder is used. + + *origin*, a ``dns.name.Name`` (or ``None``), the + origin to use for relative names. + + *relativize*, a ``bool``. If true, name will be relativized. + + *relativize_to*, a ``dns.name.Name`` (or ``None``), the origin to use + when relativizing names. If not set, the *origin* value will be used. + + Returns a ``dns.rrset.RRset`` object. + """ + + if isinstance(name, str): + name = dns.name.from_text(name, None, idna_codec=idna_codec) + rdclass = dns.rdataclass.RdataClass.make(rdclass) + rdtype = dns.rdatatype.RdataType.make(rdtype) + r = RRset(name, rdclass, rdtype) + r.update_ttl(ttl) + for t in text_rdatas: + rd = dns.rdata.from_text( + r.rdclass, r.rdtype, t, origin, relativize, relativize_to, idna_codec + ) + r.add(rd) + return r + + +def from_text( + name: dns.name.Name | str, + ttl: int, + rdclass: dns.rdataclass.RdataClass | str, + rdtype: dns.rdatatype.RdataType | str, + *text_rdatas: Any, +) -> RRset: + """Create an RRset with the specified name, TTL, class, and type and with + the specified rdatas in text format. + + Returns a ``dns.rrset.RRset`` object. + """ + + return from_text_list( + name, ttl, rdclass, rdtype, cast(Collection[str], text_rdatas) + ) + + +def from_rdata_list( + name: dns.name.Name | str, + ttl: int, + rdatas: Collection[dns.rdata.Rdata], + idna_codec: dns.name.IDNACodec | None = None, +) -> RRset: + """Create an RRset with the specified name and TTL, and with + the specified list of rdata objects. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder to use; if ``None``, the default IDNA 2003 + encoder/decoder is used. + + Returns a ``dns.rrset.RRset`` object. + + """ + + if isinstance(name, str): + name = dns.name.from_text(name, None, idna_codec=idna_codec) + + if len(rdatas) == 0: + raise ValueError("rdata list must not be empty") + r = None + for rd in rdatas: + if r is None: + r = RRset(name, rd.rdclass, rd.rdtype) + r.update_ttl(ttl) + r.add(rd) + assert r is not None + return r + + +def from_rdata(name: dns.name.Name | str, ttl: int, *rdatas: Any) -> RRset: + """Create an RRset with the specified name and TTL, and with + the specified rdata objects. + + Returns a ``dns.rrset.RRset`` object. + """ + + return from_rdata_list(name, ttl, cast(Collection[dns.rdata.Rdata], rdatas)) diff --git a/netdeploy/lib/python3.11/site-packages/dns/serial.py b/netdeploy/lib/python3.11/site-packages/dns/serial.py new file mode 100644 index 0000000..3417299 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/serial.py @@ -0,0 +1,118 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +"""Serial Number Arthimetic from RFC 1982""" + + +class Serial: + def __init__(self, value: int, bits: int = 32): + self.value = value % 2**bits + self.bits = bits + + def __repr__(self): + return f"dns.serial.Serial({self.value}, {self.bits})" + + def __eq__(self, other): + if isinstance(other, int): + other = Serial(other, self.bits) + elif not isinstance(other, Serial) or other.bits != self.bits: + return NotImplemented + return self.value == other.value + + def __ne__(self, other): + if isinstance(other, int): + other = Serial(other, self.bits) + elif not isinstance(other, Serial) or other.bits != self.bits: + return NotImplemented + return self.value != other.value + + def __lt__(self, other): + if isinstance(other, int): + other = Serial(other, self.bits) + elif not isinstance(other, Serial) or other.bits != self.bits: + return NotImplemented + if self.value < other.value and other.value - self.value < 2 ** (self.bits - 1): + return True + elif self.value > other.value and self.value - other.value > 2 ** ( + self.bits - 1 + ): + return True + else: + return False + + def __le__(self, other): + return self == other or self < other + + def __gt__(self, other): + if isinstance(other, int): + other = Serial(other, self.bits) + elif not isinstance(other, Serial) or other.bits != self.bits: + return NotImplemented + if self.value < other.value and other.value - self.value > 2 ** (self.bits - 1): + return True + elif self.value > other.value and self.value - other.value < 2 ** ( + self.bits - 1 + ): + return True + else: + return False + + def __ge__(self, other): + return self == other or self > other + + def __add__(self, other): + v = self.value + if isinstance(other, Serial): + delta = other.value + elif isinstance(other, int): + delta = other + else: + raise ValueError + if abs(delta) > (2 ** (self.bits - 1) - 1): + raise ValueError + v += delta + v = v % 2**self.bits + return Serial(v, self.bits) + + def __iadd__(self, other): + v = self.value + if isinstance(other, Serial): + delta = other.value + elif isinstance(other, int): + delta = other + else: + raise ValueError + if abs(delta) > (2 ** (self.bits - 1) - 1): + raise ValueError + v += delta + v = v % 2**self.bits + self.value = v + return self + + def __sub__(self, other): + v = self.value + if isinstance(other, Serial): + delta = other.value + elif isinstance(other, int): + delta = other + else: + raise ValueError + if abs(delta) > (2 ** (self.bits - 1) - 1): + raise ValueError + v -= delta + v = v % 2**self.bits + return Serial(v, self.bits) + + def __isub__(self, other): + v = self.value + if isinstance(other, Serial): + delta = other.value + elif isinstance(other, int): + delta = other + else: + raise ValueError + if abs(delta) > (2 ** (self.bits - 1) - 1): + raise ValueError + v -= delta + v = v % 2**self.bits + self.value = v + return self diff --git a/netdeploy/lib/python3.11/site-packages/dns/set.py b/netdeploy/lib/python3.11/site-packages/dns/set.py new file mode 100644 index 0000000..ae8f0dd --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/set.py @@ -0,0 +1,308 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +import itertools + + +class Set: + """A simple set class. + + This class was originally used to deal with python not having a set class, and + originally the class used lists in its implementation. The ordered and indexable + nature of RRsets and Rdatasets is unfortunately widely used in dnspython + applications, so for backwards compatibility sets continue to be a custom class, now + based on an ordered dictionary. + """ + + __slots__ = ["items"] + + def __init__(self, items=None): + """Initialize the set. + + *items*, an iterable or ``None``, the initial set of items. + """ + + self.items = dict() + if items is not None: + for item in items: + # This is safe for how we use set, but if other code + # subclasses it could be a legitimate issue. + self.add(item) # lgtm[py/init-calls-subclass] + + def __repr__(self): + return f"dns.set.Set({repr(list(self.items.keys()))})" # pragma: no cover + + def add(self, item): + """Add an item to the set.""" + + if item not in self.items: + self.items[item] = None + + def remove(self, item): + """Remove an item from the set.""" + + try: + del self.items[item] + except KeyError: + raise ValueError + + def discard(self, item): + """Remove an item from the set if present.""" + + self.items.pop(item, None) + + def pop(self): + """Remove an arbitrary item from the set.""" + (k, _) = self.items.popitem() + return k + + def _clone(self) -> "Set": + """Make a (shallow) copy of the set. + + There is a 'clone protocol' that subclasses of this class + should use. To make a copy, first call your super's _clone() + method, and use the object returned as the new instance. Then + make shallow copies of the attributes defined in the subclass. + + This protocol allows us to write the set algorithms that + return new instances (e.g. union) once, and keep using them in + subclasses. + """ + + if hasattr(self, "_clone_class"): + cls = self._clone_class # type: ignore + else: + cls = self.__class__ + obj = cls.__new__(cls) + obj.items = dict() + obj.items.update(self.items) + return obj + + def __copy__(self): + """Make a (shallow) copy of the set.""" + + return self._clone() + + def copy(self): + """Make a (shallow) copy of the set.""" + + return self._clone() + + def union_update(self, other): + """Update the set, adding any elements from other which are not + already in the set. + """ + + if not isinstance(other, Set): + raise ValueError("other must be a Set instance") + if self is other: # lgtm[py/comparison-using-is] + return + for item in other.items: + self.add(item) + + def intersection_update(self, other): + """Update the set, removing any elements from other which are not + in both sets. + """ + + if not isinstance(other, Set): + raise ValueError("other must be a Set instance") + if self is other: # lgtm[py/comparison-using-is] + return + # we make a copy of the list so that we can remove items from + # the list without breaking the iterator. + for item in list(self.items): + if item not in other.items: + del self.items[item] + + def difference_update(self, other): + """Update the set, removing any elements from other which are in + the set. + """ + + if not isinstance(other, Set): + raise ValueError("other must be a Set instance") + if self is other: # lgtm[py/comparison-using-is] + self.items.clear() + else: + for item in other.items: + self.discard(item) + + def symmetric_difference_update(self, other): + """Update the set, retaining only elements unique to both sets.""" + + if not isinstance(other, Set): + raise ValueError("other must be a Set instance") + if self is other: # lgtm[py/comparison-using-is] + self.items.clear() + else: + overlap = self.intersection(other) + self.union_update(other) + self.difference_update(overlap) + + def union(self, other): + """Return a new set which is the union of ``self`` and ``other``. + + Returns the same Set type as this set. + """ + + obj = self._clone() + obj.union_update(other) + return obj + + def intersection(self, other): + """Return a new set which is the intersection of ``self`` and + ``other``. + + Returns the same Set type as this set. + """ + + obj = self._clone() + obj.intersection_update(other) + return obj + + def difference(self, other): + """Return a new set which ``self`` - ``other``, i.e. the items + in ``self`` which are not also in ``other``. + + Returns the same Set type as this set. + """ + + obj = self._clone() + obj.difference_update(other) + return obj + + def symmetric_difference(self, other): + """Return a new set which (``self`` - ``other``) | (``other`` + - ``self), ie: the items in either ``self`` or ``other`` which + are not contained in their intersection. + + Returns the same Set type as this set. + """ + + obj = self._clone() + obj.symmetric_difference_update(other) + return obj + + def __or__(self, other): + return self.union(other) + + def __and__(self, other): + return self.intersection(other) + + def __add__(self, other): + return self.union(other) + + def __sub__(self, other): + return self.difference(other) + + def __xor__(self, other): + return self.symmetric_difference(other) + + def __ior__(self, other): + self.union_update(other) + return self + + def __iand__(self, other): + self.intersection_update(other) + return self + + def __iadd__(self, other): + self.union_update(other) + return self + + def __isub__(self, other): + self.difference_update(other) + return self + + def __ixor__(self, other): + self.symmetric_difference_update(other) + return self + + def update(self, other): + """Update the set, adding any elements from other which are not + already in the set. + + *other*, the collection of items with which to update the set, which + may be any iterable type. + """ + + for item in other: + self.add(item) + + def clear(self): + """Make the set empty.""" + self.items.clear() + + def __eq__(self, other): + return self.items == other.items + + def __ne__(self, other): + return not self.__eq__(other) + + def __len__(self): + return len(self.items) + + def __iter__(self): + return iter(self.items) + + def __getitem__(self, i): + if isinstance(i, slice): + return list(itertools.islice(self.items, i.start, i.stop, i.step)) + else: + return next(itertools.islice(self.items, i, i + 1)) + + def __delitem__(self, i): + if isinstance(i, slice): + for elt in list(self[i]): + del self.items[elt] + else: + del self.items[self[i]] + + def issubset(self, other): + """Is this set a subset of *other*? + + Returns a ``bool``. + """ + + if not isinstance(other, Set): + raise ValueError("other must be a Set instance") + for item in self.items: + if item not in other.items: + return False + return True + + def issuperset(self, other): + """Is this set a superset of *other*? + + Returns a ``bool``. + """ + + if not isinstance(other, Set): + raise ValueError("other must be a Set instance") + for item in other.items: + if item not in self.items: + return False + return True + + def isdisjoint(self, other): + if not isinstance(other, Set): + raise ValueError("other must be a Set instance") + for item in other.items: + if item in self.items: + return False + return True diff --git a/netdeploy/lib/python3.11/site-packages/dns/tokenizer.py b/netdeploy/lib/python3.11/site-packages/dns/tokenizer.py new file mode 100644 index 0000000..86ae3e2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/tokenizer.py @@ -0,0 +1,706 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""Tokenize DNS zone file format""" + +import io +import sys +from typing import Any, List, Tuple + +import dns.exception +import dns.name +import dns.ttl + +_DELIMITERS = {" ", "\t", "\n", ";", "(", ")", '"'} +_QUOTING_DELIMITERS = {'"'} + +EOF = 0 +EOL = 1 +WHITESPACE = 2 +IDENTIFIER = 3 +QUOTED_STRING = 4 +COMMENT = 5 +DELIMITER = 6 + + +class UngetBufferFull(dns.exception.DNSException): + """An attempt was made to unget a token when the unget buffer was full.""" + + +class Token: + """A DNS zone file format token. + + ttype: The token type + value: The token value + has_escape: Does the token value contain escapes? + """ + + def __init__( + self, + ttype: int, + value: Any = "", + has_escape: bool = False, + comment: str | None = None, + ): + """Initialize a token instance.""" + + self.ttype = ttype + self.value = value + self.has_escape = has_escape + self.comment = comment + + def is_eof(self) -> bool: + return self.ttype == EOF + + def is_eol(self) -> bool: + return self.ttype == EOL + + def is_whitespace(self) -> bool: + return self.ttype == WHITESPACE + + def is_identifier(self) -> bool: + return self.ttype == IDENTIFIER + + def is_quoted_string(self) -> bool: + return self.ttype == QUOTED_STRING + + def is_comment(self) -> bool: + return self.ttype == COMMENT + + def is_delimiter(self) -> bool: # pragma: no cover (we don't return delimiters yet) + return self.ttype == DELIMITER + + def is_eol_or_eof(self) -> bool: + return self.ttype == EOL or self.ttype == EOF + + def __eq__(self, other): + if not isinstance(other, Token): + return False + return self.ttype == other.ttype and self.value == other.value + + def __ne__(self, other): + if not isinstance(other, Token): + return True + return self.ttype != other.ttype or self.value != other.value + + def __str__(self): + return f'{self.ttype} "{self.value}"' + + def unescape(self) -> "Token": + if not self.has_escape: + return self + unescaped = "" + l = len(self.value) + i = 0 + while i < l: + c = self.value[i] + i += 1 + if c == "\\": + if i >= l: # pragma: no cover (can't happen via get()) + raise dns.exception.UnexpectedEnd + c = self.value[i] + i += 1 + if c.isdigit(): + if i >= l: + raise dns.exception.UnexpectedEnd + c2 = self.value[i] + i += 1 + if i >= l: + raise dns.exception.UnexpectedEnd + c3 = self.value[i] + i += 1 + if not (c2.isdigit() and c3.isdigit()): + raise dns.exception.SyntaxError + codepoint = int(c) * 100 + int(c2) * 10 + int(c3) + if codepoint > 255: + raise dns.exception.SyntaxError + c = chr(codepoint) + unescaped += c + return Token(self.ttype, unescaped) + + def unescape_to_bytes(self) -> "Token": + # We used to use unescape() for TXT-like records, but this + # caused problems as we'd process DNS escapes into Unicode code + # points instead of byte values, and then a to_text() of the + # processed data would not equal the original input. For + # example, \226 in the TXT record would have a to_text() of + # \195\162 because we applied UTF-8 encoding to Unicode code + # point 226. + # + # We now apply escapes while converting directly to bytes, + # avoiding this double encoding. + # + # This code also handles cases where the unicode input has + # non-ASCII code-points in it by converting it to UTF-8. TXT + # records aren't defined for Unicode, but this is the best we + # can do to preserve meaning. For example, + # + # foo\u200bbar + # + # (where \u200b is Unicode code point 0x200b) will be treated + # as if the input had been the UTF-8 encoding of that string, + # namely: + # + # foo\226\128\139bar + # + unescaped = b"" + l = len(self.value) + i = 0 + while i < l: + c = self.value[i] + i += 1 + if c == "\\": + if i >= l: # pragma: no cover (can't happen via get()) + raise dns.exception.UnexpectedEnd + c = self.value[i] + i += 1 + if c.isdigit(): + if i >= l: + raise dns.exception.UnexpectedEnd + c2 = self.value[i] + i += 1 + if i >= l: + raise dns.exception.UnexpectedEnd + c3 = self.value[i] + i += 1 + if not (c2.isdigit() and c3.isdigit()): + raise dns.exception.SyntaxError + codepoint = int(c) * 100 + int(c2) * 10 + int(c3) + if codepoint > 255: + raise dns.exception.SyntaxError + unescaped += b"%c" % (codepoint) + else: + # Note that as mentioned above, if c is a Unicode + # code point outside of the ASCII range, then this + # += is converting that code point to its UTF-8 + # encoding and appending multiple bytes to + # unescaped. + unescaped += c.encode() + else: + unescaped += c.encode() + return Token(self.ttype, bytes(unescaped)) + + +class Tokenizer: + """A DNS zone file format tokenizer. + + A token object is basically a (type, value) tuple. The valid + types are EOF, EOL, WHITESPACE, IDENTIFIER, QUOTED_STRING, + COMMENT, and DELIMITER. + + file: The file to tokenize + + ungotten_char: The most recently ungotten character, or None. + + ungotten_token: The most recently ungotten token, or None. + + multiline: The current multiline level. This value is increased + by one every time a '(' delimiter is read, and decreased by one every time + a ')' delimiter is read. + + quoting: This variable is true if the tokenizer is currently + reading a quoted string. + + eof: This variable is true if the tokenizer has encountered EOF. + + delimiters: The current delimiter dictionary. + + line_number: The current line number + + filename: A filename that will be returned by the where() method. + + idna_codec: A dns.name.IDNACodec, specifies the IDNA + encoder/decoder. If None, the default IDNA 2003 + encoder/decoder is used. + """ + + def __init__( + self, + f: Any = sys.stdin, + filename: str | None = None, + idna_codec: dns.name.IDNACodec | None = None, + ): + """Initialize a tokenizer instance. + + f: The file to tokenize. The default is sys.stdin. + This parameter may also be a string, in which case the tokenizer + will take its input from the contents of the string. + + filename: the name of the filename that the where() method + will return. + + idna_codec: A dns.name.IDNACodec, specifies the IDNA + encoder/decoder. If None, the default IDNA 2003 + encoder/decoder is used. + """ + + if isinstance(f, str): + f = io.StringIO(f) + if filename is None: + filename = "" + elif isinstance(f, bytes): + f = io.StringIO(f.decode()) + if filename is None: + filename = "" + else: + if filename is None: + if f is sys.stdin: + filename = "" + else: + filename = "" + self.file = f + self.ungotten_char: str | None = None + self.ungotten_token: Token | None = None + self.multiline = 0 + self.quoting = False + self.eof = False + self.delimiters = _DELIMITERS + self.line_number = 1 + assert filename is not None + self.filename = filename + if idna_codec is None: + self.idna_codec: dns.name.IDNACodec = dns.name.IDNA_2003 + else: + self.idna_codec = idna_codec + + def _get_char(self) -> str: + """Read a character from input.""" + + if self.ungotten_char is None: + if self.eof: + c = "" + else: + c = self.file.read(1) + if c == "": + self.eof = True + elif c == "\n": + self.line_number += 1 + else: + c = self.ungotten_char + self.ungotten_char = None + return c + + def where(self) -> Tuple[str, int]: + """Return the current location in the input. + + Returns a (string, int) tuple. The first item is the filename of + the input, the second is the current line number. + """ + + return (self.filename, self.line_number) + + def _unget_char(self, c: str) -> None: + """Unget a character. + + The unget buffer for characters is only one character large; it is + an error to try to unget a character when the unget buffer is not + empty. + + c: the character to unget + raises UngetBufferFull: there is already an ungotten char + """ + + if self.ungotten_char is not None: + # this should never happen! + raise UngetBufferFull # pragma: no cover + self.ungotten_char = c + + def skip_whitespace(self) -> int: + """Consume input until a non-whitespace character is encountered. + + The non-whitespace character is then ungotten, and the number of + whitespace characters consumed is returned. + + If the tokenizer is in multiline mode, then newlines are whitespace. + + Returns the number of characters skipped. + """ + + skipped = 0 + while True: + c = self._get_char() + if c != " " and c != "\t": + if (c != "\n") or not self.multiline: + self._unget_char(c) + return skipped + skipped += 1 + + def get(self, want_leading: bool = False, want_comment: bool = False) -> Token: + """Get the next token. + + want_leading: If True, return a WHITESPACE token if the + first character read is whitespace. The default is False. + + want_comment: If True, return a COMMENT token if the + first token read is a comment. The default is False. + + Raises dns.exception.UnexpectedEnd: input ended prematurely + + Raises dns.exception.SyntaxError: input was badly formed + + Returns a Token. + """ + + if self.ungotten_token is not None: + utoken = self.ungotten_token + self.ungotten_token = None + if utoken.is_whitespace(): + if want_leading: + return utoken + elif utoken.is_comment(): + if want_comment: + return utoken + else: + return utoken + skipped = self.skip_whitespace() + if want_leading and skipped > 0: + return Token(WHITESPACE, " ") + token = "" + ttype = IDENTIFIER + has_escape = False + while True: + c = self._get_char() + if c == "" or c in self.delimiters: + if c == "" and self.quoting: + raise dns.exception.UnexpectedEnd + if token == "" and ttype != QUOTED_STRING: + if c == "(": + self.multiline += 1 + self.skip_whitespace() + continue + elif c == ")": + if self.multiline <= 0: + raise dns.exception.SyntaxError + self.multiline -= 1 + self.skip_whitespace() + continue + elif c == '"': + if not self.quoting: + self.quoting = True + self.delimiters = _QUOTING_DELIMITERS + ttype = QUOTED_STRING + continue + else: + self.quoting = False + self.delimiters = _DELIMITERS + self.skip_whitespace() + continue + elif c == "\n": + return Token(EOL, "\n") + elif c == ";": + while 1: + c = self._get_char() + if c == "\n" or c == "": + break + token += c + if want_comment: + self._unget_char(c) + return Token(COMMENT, token) + elif c == "": + if self.multiline: + raise dns.exception.SyntaxError( + "unbalanced parentheses" + ) + return Token(EOF, comment=token) + elif self.multiline: + self.skip_whitespace() + token = "" + continue + else: + return Token(EOL, "\n", comment=token) + else: + # This code exists in case we ever want a + # delimiter to be returned. It never produces + # a token currently. + token = c + ttype = DELIMITER + else: + self._unget_char(c) + break + elif self.quoting and c == "\n": + raise dns.exception.SyntaxError("newline in quoted string") + elif c == "\\": + # + # It's an escape. Put it and the next character into + # the token; it will be checked later for goodness. + # + token += c + has_escape = True + c = self._get_char() + if c == "" or (c == "\n" and not self.quoting): + raise dns.exception.UnexpectedEnd + token += c + if token == "" and ttype != QUOTED_STRING: + if self.multiline: + raise dns.exception.SyntaxError("unbalanced parentheses") + ttype = EOF + return Token(ttype, token, has_escape) + + def unget(self, token: Token) -> None: + """Unget a token. + + The unget buffer for tokens is only one token large; it is + an error to try to unget a token when the unget buffer is not + empty. + + token: the token to unget + + Raises UngetBufferFull: there is already an ungotten token + """ + + if self.ungotten_token is not None: + raise UngetBufferFull + self.ungotten_token = token + + def next(self): + """Return the next item in an iteration. + + Returns a Token. + """ + + token = self.get() + if token.is_eof(): + raise StopIteration + return token + + __next__ = next + + def __iter__(self): + return self + + # Helpers + + def get_int(self, base: int = 10) -> int: + """Read the next token and interpret it as an unsigned integer. + + Raises dns.exception.SyntaxError if not an unsigned integer. + + Returns an int. + """ + + token = self.get().unescape() + if not token.is_identifier(): + raise dns.exception.SyntaxError("expecting an identifier") + if not token.value.isdigit(): + raise dns.exception.SyntaxError("expecting an integer") + return int(token.value, base) + + def get_uint8(self) -> int: + """Read the next token and interpret it as an 8-bit unsigned + integer. + + Raises dns.exception.SyntaxError if not an 8-bit unsigned integer. + + Returns an int. + """ + + value = self.get_int() + if value < 0 or value > 255: + raise dns.exception.SyntaxError(f"{value} is not an unsigned 8-bit integer") + return value + + def get_uint16(self, base: int = 10) -> int: + """Read the next token and interpret it as a 16-bit unsigned + integer. + + Raises dns.exception.SyntaxError if not a 16-bit unsigned integer. + + Returns an int. + """ + + value = self.get_int(base=base) + if value < 0 or value > 65535: + if base == 8: + raise dns.exception.SyntaxError( + f"{value:o} is not an octal unsigned 16-bit integer" + ) + else: + raise dns.exception.SyntaxError( + f"{value} is not an unsigned 16-bit integer" + ) + return value + + def get_uint32(self, base: int = 10) -> int: + """Read the next token and interpret it as a 32-bit unsigned + integer. + + Raises dns.exception.SyntaxError if not a 32-bit unsigned integer. + + Returns an int. + """ + + value = self.get_int(base=base) + if value < 0 or value > 4294967295: + raise dns.exception.SyntaxError( + f"{value} is not an unsigned 32-bit integer" + ) + return value + + def get_uint48(self, base: int = 10) -> int: + """Read the next token and interpret it as a 48-bit unsigned + integer. + + Raises dns.exception.SyntaxError if not a 48-bit unsigned integer. + + Returns an int. + """ + + value = self.get_int(base=base) + if value < 0 or value > 281474976710655: + raise dns.exception.SyntaxError( + f"{value} is not an unsigned 48-bit integer" + ) + return value + + def get_string(self, max_length: int | None = None) -> str: + """Read the next token and interpret it as a string. + + Raises dns.exception.SyntaxError if not a string. + Raises dns.exception.SyntaxError if token value length + exceeds max_length (if specified). + + Returns a string. + """ + + token = self.get().unescape() + if not (token.is_identifier() or token.is_quoted_string()): + raise dns.exception.SyntaxError("expecting a string") + if max_length and len(token.value) > max_length: + raise dns.exception.SyntaxError("string too long") + return token.value + + def get_identifier(self) -> str: + """Read the next token, which should be an identifier. + + Raises dns.exception.SyntaxError if not an identifier. + + Returns a string. + """ + + token = self.get().unescape() + if not token.is_identifier(): + raise dns.exception.SyntaxError("expecting an identifier") + return token.value + + def get_remaining(self, max_tokens: int | None = None) -> List[Token]: + """Return the remaining tokens on the line, until an EOL or EOF is seen. + + max_tokens: If not None, stop after this number of tokens. + + Returns a list of tokens. + """ + + tokens = [] + while True: + token = self.get() + if token.is_eol_or_eof(): + self.unget(token) + break + tokens.append(token) + if len(tokens) == max_tokens: + break + return tokens + + def concatenate_remaining_identifiers(self, allow_empty: bool = False) -> str: + """Read the remaining tokens on the line, which should be identifiers. + + Raises dns.exception.SyntaxError if there are no remaining tokens, + unless `allow_empty=True` is given. + + Raises dns.exception.SyntaxError if a token is seen that is not an + identifier. + + Returns a string containing a concatenation of the remaining + identifiers. + """ + s = "" + while True: + token = self.get().unescape() + if token.is_eol_or_eof(): + self.unget(token) + break + if not token.is_identifier(): + raise dns.exception.SyntaxError + s += token.value + if not (allow_empty or s): + raise dns.exception.SyntaxError("expecting another identifier") + return s + + def as_name( + self, + token: Token, + origin: dns.name.Name | None = None, + relativize: bool = False, + relativize_to: dns.name.Name | None = None, + ) -> dns.name.Name: + """Try to interpret the token as a DNS name. + + Raises dns.exception.SyntaxError if not a name. + + Returns a dns.name.Name. + """ + if not token.is_identifier(): + raise dns.exception.SyntaxError("expecting an identifier") + name = dns.name.from_text(token.value, origin, self.idna_codec) + return name.choose_relativity(relativize_to or origin, relativize) + + def get_name( + self, + origin: dns.name.Name | None = None, + relativize: bool = False, + relativize_to: dns.name.Name | None = None, + ) -> dns.name.Name: + """Read the next token and interpret it as a DNS name. + + Raises dns.exception.SyntaxError if not a name. + + Returns a dns.name.Name. + """ + + token = self.get() + return self.as_name(token, origin, relativize, relativize_to) + + def get_eol_as_token(self) -> Token: + """Read the next token and raise an exception if it isn't EOL or + EOF. + + Returns a string. + """ + + token = self.get() + if not token.is_eol_or_eof(): + raise dns.exception.SyntaxError( + f'expected EOL or EOF, got {token.ttype} "{token.value}"' + ) + return token + + def get_eol(self) -> str: + return self.get_eol_as_token().value + + def get_ttl(self) -> int: + """Read the next token and interpret it as a DNS TTL. + + Raises dns.exception.SyntaxError or dns.ttl.BadTTL if not an + identifier or badly formed. + + Returns an int. + """ + + token = self.get().unescape() + if not token.is_identifier(): + raise dns.exception.SyntaxError("expecting an identifier") + return dns.ttl.from_text(token.value) diff --git a/netdeploy/lib/python3.11/site-packages/dns/transaction.py b/netdeploy/lib/python3.11/site-packages/dns/transaction.py new file mode 100644 index 0000000..9ecd737 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/transaction.py @@ -0,0 +1,651 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import collections +from typing import Any, Callable, Iterator, List, Tuple + +import dns.exception +import dns.name +import dns.node +import dns.rdata +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.rrset +import dns.serial +import dns.ttl + + +class TransactionManager: + def reader(self) -> "Transaction": + """Begin a read-only transaction.""" + raise NotImplementedError # pragma: no cover + + def writer(self, replacement: bool = False) -> "Transaction": + """Begin a writable transaction. + + *replacement*, a ``bool``. If `True`, the content of the + transaction completely replaces any prior content. If False, + the default, then the content of the transaction updates the + existing content. + """ + raise NotImplementedError # pragma: no cover + + def origin_information( + self, + ) -> Tuple[dns.name.Name | None, bool, dns.name.Name | None]: + """Returns a tuple + + (absolute_origin, relativize, effective_origin) + + giving the absolute name of the default origin for any + relative domain names, the "effective origin", and whether + names should be relativized. The "effective origin" is the + absolute origin if relativize is False, and the empty name if + relativize is true. (The effective origin is provided even + though it can be computed from the absolute_origin and + relativize setting because it avoids a lot of code + duplication.) + + If the returned names are `None`, then no origin information is + available. + + This information is used by code working with transactions to + allow it to coordinate relativization. The transaction code + itself takes what it gets (i.e. does not change name + relativity). + + """ + raise NotImplementedError # pragma: no cover + + def get_class(self) -> dns.rdataclass.RdataClass: + """The class of the transaction manager.""" + raise NotImplementedError # pragma: no cover + + def from_wire_origin(self) -> dns.name.Name | None: + """Origin to use in from_wire() calls.""" + (absolute_origin, relativize, _) = self.origin_information() + if relativize: + return absolute_origin + else: + return None + + +class DeleteNotExact(dns.exception.DNSException): + """Existing data did not match data specified by an exact delete.""" + + +class ReadOnly(dns.exception.DNSException): + """Tried to write to a read-only transaction.""" + + +class AlreadyEnded(dns.exception.DNSException): + """Tried to use an already-ended transaction.""" + + +def _ensure_immutable_rdataset(rdataset): + if rdataset is None or isinstance(rdataset, dns.rdataset.ImmutableRdataset): + return rdataset + return dns.rdataset.ImmutableRdataset(rdataset) + + +def _ensure_immutable_node(node): + if node is None or node.is_immutable(): + return node + return dns.node.ImmutableNode(node) + + +CheckPutRdatasetType = Callable[ + ["Transaction", dns.name.Name, dns.rdataset.Rdataset], None +] +CheckDeleteRdatasetType = Callable[ + ["Transaction", dns.name.Name, dns.rdatatype.RdataType, dns.rdatatype.RdataType], + None, +] +CheckDeleteNameType = Callable[["Transaction", dns.name.Name], None] + + +class Transaction: + def __init__( + self, + manager: TransactionManager, + replacement: bool = False, + read_only: bool = False, + ): + self.manager = manager + self.replacement = replacement + self.read_only = read_only + self._ended = False + self._check_put_rdataset: List[CheckPutRdatasetType] = [] + self._check_delete_rdataset: List[CheckDeleteRdatasetType] = [] + self._check_delete_name: List[CheckDeleteNameType] = [] + + # + # This is the high level API + # + # Note that we currently use non-immutable types in the return type signature to + # avoid covariance problems, e.g. if the caller has a List[Rdataset], mypy will be + # unhappy if we return an ImmutableRdataset. + + def get( + self, + name: dns.name.Name | str | None, + rdtype: dns.rdatatype.RdataType | str, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + ) -> dns.rdataset.Rdataset: + """Return the rdataset associated with *name*, *rdtype*, and *covers*, + or `None` if not found. + + Note that the returned rdataset is immutable. + """ + self._check_ended() + if isinstance(name, str): + name = dns.name.from_text(name, None) + rdtype = dns.rdatatype.RdataType.make(rdtype) + covers = dns.rdatatype.RdataType.make(covers) + rdataset = self._get_rdataset(name, rdtype, covers) + return _ensure_immutable_rdataset(rdataset) + + def get_node(self, name: dns.name.Name) -> dns.node.Node | None: + """Return the node at *name*, if any. + + Returns an immutable node or ``None``. + """ + return _ensure_immutable_node(self._get_node(name)) + + def _check_read_only(self) -> None: + if self.read_only: + raise ReadOnly + + def add(self, *args: Any) -> None: + """Add records. + + The arguments may be: + + - rrset + + - name, rdataset... + + - name, ttl, rdata... + """ + self._check_ended() + self._check_read_only() + self._add(False, args) + + def replace(self, *args: Any) -> None: + """Replace the existing rdataset at the name with the specified + rdataset, or add the specified rdataset if there was no existing + rdataset. + + The arguments may be: + + - rrset + + - name, rdataset... + + - name, ttl, rdata... + + Note that if you want to replace the entire node, you should do + a delete of the name followed by one or more calls to add() or + replace(). + """ + self._check_ended() + self._check_read_only() + self._add(True, args) + + def delete(self, *args: Any) -> None: + """Delete records. + + It is not an error if some of the records are not in the existing + set. + + The arguments may be: + + - rrset + + - name + + - name, rdatatype, [covers] + + - name, rdataset... + + - name, rdata... + """ + self._check_ended() + self._check_read_only() + self._delete(False, args) + + def delete_exact(self, *args: Any) -> None: + """Delete records. + + The arguments may be: + + - rrset + + - name + + - name, rdatatype, [covers] + + - name, rdataset... + + - name, rdata... + + Raises dns.transaction.DeleteNotExact if some of the records + are not in the existing set. + + """ + self._check_ended() + self._check_read_only() + self._delete(True, args) + + def name_exists(self, name: dns.name.Name | str) -> bool: + """Does the specified name exist?""" + self._check_ended() + if isinstance(name, str): + name = dns.name.from_text(name, None) + return self._name_exists(name) + + def update_serial( + self, + value: int = 1, + relative: bool = True, + name: dns.name.Name = dns.name.empty, + ) -> None: + """Update the serial number. + + *value*, an `int`, is an increment if *relative* is `True`, or the + actual value to set if *relative* is `False`. + + Raises `KeyError` if there is no SOA rdataset at *name*. + + Raises `ValueError` if *value* is negative or if the increment is + so large that it would cause the new serial to be less than the + prior value. + """ + self._check_ended() + if value < 0: + raise ValueError("negative update_serial() value") + if isinstance(name, str): + name = dns.name.from_text(name, None) + rdataset = self._get_rdataset(name, dns.rdatatype.SOA, dns.rdatatype.NONE) + if rdataset is None or len(rdataset) == 0: + raise KeyError + if relative: + serial = dns.serial.Serial(rdataset[0].serial) + value + else: + serial = dns.serial.Serial(value) + serial = serial.value # convert back to int + if serial == 0: + serial = 1 + rdata = rdataset[0].replace(serial=serial) + new_rdataset = dns.rdataset.from_rdata(rdataset.ttl, rdata) + self.replace(name, new_rdataset) + + def __iter__(self): + self._check_ended() + return self._iterate_rdatasets() + + def changed(self) -> bool: + """Has this transaction changed anything? + + For read-only transactions, the result is always `False`. + + For writable transactions, the result is `True` if at some time + during the life of the transaction, the content was changed. + """ + self._check_ended() + return self._changed() + + def commit(self) -> None: + """Commit the transaction. + + Normally transactions are used as context managers and commit + or rollback automatically, but it may be done explicitly if needed. + A ``dns.transaction.Ended`` exception will be raised if you try + to use a transaction after it has been committed or rolled back. + + Raises an exception if the commit fails (in which case the transaction + is also rolled back. + """ + self._end(True) + + def rollback(self) -> None: + """Rollback the transaction. + + Normally transactions are used as context managers and commit + or rollback automatically, but it may be done explicitly if needed. + A ``dns.transaction.AlreadyEnded`` exception will be raised if you try + to use a transaction after it has been committed or rolled back. + + Rollback cannot otherwise fail. + """ + self._end(False) + + def check_put_rdataset(self, check: CheckPutRdatasetType) -> None: + """Call *check* before putting (storing) an rdataset. + + The function is called with the transaction, the name, and the rdataset. + + The check function may safely make non-mutating transaction method + calls, but behavior is undefined if mutating transaction methods are + called. The check function should raise an exception if it objects to + the put, and otherwise should return ``None``. + """ + self._check_put_rdataset.append(check) + + def check_delete_rdataset(self, check: CheckDeleteRdatasetType) -> None: + """Call *check* before deleting an rdataset. + + The function is called with the transaction, the name, the rdatatype, + and the covered rdatatype. + + The check function may safely make non-mutating transaction method + calls, but behavior is undefined if mutating transaction methods are + called. The check function should raise an exception if it objects to + the put, and otherwise should return ``None``. + """ + self._check_delete_rdataset.append(check) + + def check_delete_name(self, check: CheckDeleteNameType) -> None: + """Call *check* before putting (storing) an rdataset. + + The function is called with the transaction and the name. + + The check function may safely make non-mutating transaction method + calls, but behavior is undefined if mutating transaction methods are + called. The check function should raise an exception if it objects to + the put, and otherwise should return ``None``. + """ + self._check_delete_name.append(check) + + def iterate_rdatasets( + self, + ) -> Iterator[Tuple[dns.name.Name, dns.rdataset.Rdataset]]: + """Iterate all the rdatasets in the transaction, returning + (`dns.name.Name`, `dns.rdataset.Rdataset`) tuples. + + Note that as is usual with python iterators, adding or removing items + while iterating will invalidate the iterator and may raise `RuntimeError` + or fail to iterate over all entries.""" + self._check_ended() + return self._iterate_rdatasets() + + def iterate_names(self) -> Iterator[dns.name.Name]: + """Iterate all the names in the transaction. + + Note that as is usual with python iterators, adding or removing names + while iterating will invalidate the iterator and may raise `RuntimeError` + or fail to iterate over all entries.""" + self._check_ended() + return self._iterate_names() + + # + # Helper methods + # + + def _raise_if_not_empty(self, method, args): + if len(args) != 0: + raise TypeError(f"extra parameters to {method}") + + def _rdataset_from_args(self, method, deleting, args): + try: + arg = args.popleft() + if isinstance(arg, dns.rrset.RRset): + rdataset = arg.to_rdataset() + elif isinstance(arg, dns.rdataset.Rdataset): + rdataset = arg + else: + if deleting: + ttl = 0 + else: + if isinstance(arg, int): + ttl = arg + if ttl > dns.ttl.MAX_TTL: + raise ValueError(f"{method}: TTL value too big") + else: + raise TypeError(f"{method}: expected a TTL") + arg = args.popleft() + if isinstance(arg, dns.rdata.Rdata): + rdataset = dns.rdataset.from_rdata(ttl, arg) + else: + raise TypeError(f"{method}: expected an Rdata") + return rdataset + except IndexError: + if deleting: + return None + else: + # reraise + raise TypeError(f"{method}: expected more arguments") + + def _add(self, replace, args): + if replace: + method = "replace()" + else: + method = "add()" + try: + args = collections.deque(args) + arg = args.popleft() + if isinstance(arg, str): + arg = dns.name.from_text(arg, None) + if isinstance(arg, dns.name.Name): + name = arg + rdataset = self._rdataset_from_args(method, False, args) + elif isinstance(arg, dns.rrset.RRset): + rrset = arg + name = rrset.name + # rrsets are also rdatasets, but they don't print the + # same and can't be stored in nodes, so convert. + rdataset = rrset.to_rdataset() + else: + raise TypeError( + f"{method} requires a name or RRset as the first argument" + ) + assert rdataset is not None # for type checkers + if rdataset.rdclass != self.manager.get_class(): + raise ValueError(f"{method} has objects of wrong RdataClass") + if rdataset.rdtype == dns.rdatatype.SOA: + (_, _, origin) = self._origin_information() + if name != origin: + raise ValueError(f"{method} has non-origin SOA") + self._raise_if_not_empty(method, args) + if not replace: + existing = self._get_rdataset(name, rdataset.rdtype, rdataset.covers) + if existing is not None: + if isinstance(existing, dns.rdataset.ImmutableRdataset): + trds = dns.rdataset.Rdataset( + existing.rdclass, existing.rdtype, existing.covers + ) + trds.update(existing) + existing = trds + rdataset = existing.union(rdataset) + self._checked_put_rdataset(name, rdataset) + except IndexError: + raise TypeError(f"not enough parameters to {method}") + + def _delete(self, exact, args): + if exact: + method = "delete_exact()" + else: + method = "delete()" + try: + args = collections.deque(args) + arg = args.popleft() + if isinstance(arg, str): + arg = dns.name.from_text(arg, None) + if isinstance(arg, dns.name.Name): + name = arg + if len(args) > 0 and ( + isinstance(args[0], int) or isinstance(args[0], str) + ): + # deleting by type and (optionally) covers + rdtype = dns.rdatatype.RdataType.make(args.popleft()) + if len(args) > 0: + covers = dns.rdatatype.RdataType.make(args.popleft()) + else: + covers = dns.rdatatype.NONE + self._raise_if_not_empty(method, args) + existing = self._get_rdataset(name, rdtype, covers) + if existing is None: + if exact: + raise DeleteNotExact(f"{method}: missing rdataset") + else: + self._checked_delete_rdataset(name, rdtype, covers) + return + else: + rdataset = self._rdataset_from_args(method, True, args) + elif isinstance(arg, dns.rrset.RRset): + rdataset = arg # rrsets are also rdatasets + name = rdataset.name + else: + raise TypeError( + f"{method} requires a name or RRset as the first argument" + ) + self._raise_if_not_empty(method, args) + if rdataset: + if rdataset.rdclass != self.manager.get_class(): + raise ValueError(f"{method} has objects of wrong RdataClass") + existing = self._get_rdataset(name, rdataset.rdtype, rdataset.covers) + if existing is not None: + if exact: + intersection = existing.intersection(rdataset) + if intersection != rdataset: + raise DeleteNotExact(f"{method}: missing rdatas") + rdataset = existing.difference(rdataset) + if len(rdataset) == 0: + self._checked_delete_rdataset( + name, rdataset.rdtype, rdataset.covers + ) + else: + self._checked_put_rdataset(name, rdataset) + elif exact: + raise DeleteNotExact(f"{method}: missing rdataset") + else: + if exact and not self._name_exists(name): + raise DeleteNotExact(f"{method}: name not known") + self._checked_delete_name(name) + except IndexError: + raise TypeError(f"not enough parameters to {method}") + + def _check_ended(self): + if self._ended: + raise AlreadyEnded + + def _end(self, commit): + self._check_ended() + try: + self._end_transaction(commit) + finally: + self._ended = True + + def _checked_put_rdataset(self, name, rdataset): + for check in self._check_put_rdataset: + check(self, name, rdataset) + self._put_rdataset(name, rdataset) + + def _checked_delete_rdataset(self, name, rdtype, covers): + for check in self._check_delete_rdataset: + check(self, name, rdtype, covers) + self._delete_rdataset(name, rdtype, covers) + + def _checked_delete_name(self, name): + for check in self._check_delete_name: + check(self, name) + self._delete_name(name) + + # + # Transactions are context managers. + # + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if not self._ended: + if exc_type is None: + self.commit() + else: + self.rollback() + return False + + # + # This is the low level API, which must be implemented by subclasses + # of Transaction. + # + + def _get_rdataset(self, name, rdtype, covers): + """Return the rdataset associated with *name*, *rdtype*, and *covers*, + or `None` if not found. + """ + raise NotImplementedError # pragma: no cover + + def _put_rdataset(self, name, rdataset): + """Store the rdataset.""" + raise NotImplementedError # pragma: no cover + + def _delete_name(self, name): + """Delete all data associated with *name*. + + It is not an error if the name does not exist. + """ + raise NotImplementedError # pragma: no cover + + def _delete_rdataset(self, name, rdtype, covers): + """Delete all data associated with *name*, *rdtype*, and *covers*. + + It is not an error if the rdataset does not exist. + """ + raise NotImplementedError # pragma: no cover + + def _name_exists(self, name): + """Does name exist? + + Returns a bool. + """ + raise NotImplementedError # pragma: no cover + + def _changed(self): + """Has this transaction changed anything?""" + raise NotImplementedError # pragma: no cover + + def _end_transaction(self, commit): + """End the transaction. + + *commit*, a bool. If ``True``, commit the transaction, otherwise + roll it back. + + If committing and the commit fails, then roll back and raise an + exception. + """ + raise NotImplementedError # pragma: no cover + + def _set_origin(self, origin): + """Set the origin. + + This method is called when reading a possibly relativized + source, and an origin setting operation occurs (e.g. $ORIGIN + in a zone file). + """ + raise NotImplementedError # pragma: no cover + + def _iterate_rdatasets(self): + """Return an iterator that yields (name, rdataset) tuples.""" + raise NotImplementedError # pragma: no cover + + def _iterate_names(self): + """Return an iterator that yields a name.""" + raise NotImplementedError # pragma: no cover + + def _get_node(self, name): + """Return the node at *name*, if any. + + Returns a node or ``None``. + """ + raise NotImplementedError # pragma: no cover + + # + # Low-level API with a default implementation, in case a subclass needs + # to override. + # + + def _origin_information(self): + # This is only used by _add() + return self.manager.origin_information() diff --git a/netdeploy/lib/python3.11/site-packages/dns/tsig.py b/netdeploy/lib/python3.11/site-packages/dns/tsig.py new file mode 100644 index 0000000..333f9aa --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/tsig.py @@ -0,0 +1,359 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2001-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS TSIG support.""" + +import base64 +import hashlib +import hmac +import struct + +import dns.exception +import dns.name +import dns.rcode +import dns.rdataclass +import dns.rdatatype + + +class BadTime(dns.exception.DNSException): + """The current time is not within the TSIG's validity time.""" + + +class BadSignature(dns.exception.DNSException): + """The TSIG signature fails to verify.""" + + +class BadKey(dns.exception.DNSException): + """The TSIG record owner name does not match the key.""" + + +class BadAlgorithm(dns.exception.DNSException): + """The TSIG algorithm does not match the key.""" + + +class PeerError(dns.exception.DNSException): + """Base class for all TSIG errors generated by the remote peer""" + + +class PeerBadKey(PeerError): + """The peer didn't know the key we used""" + + +class PeerBadSignature(PeerError): + """The peer didn't like the signature we sent""" + + +class PeerBadTime(PeerError): + """The peer didn't like the time we sent""" + + +class PeerBadTruncation(PeerError): + """The peer didn't like amount of truncation in the TSIG we sent""" + + +# TSIG Algorithms + +HMAC_MD5 = dns.name.from_text("HMAC-MD5.SIG-ALG.REG.INT") +HMAC_SHA1 = dns.name.from_text("hmac-sha1") +HMAC_SHA224 = dns.name.from_text("hmac-sha224") +HMAC_SHA256 = dns.name.from_text("hmac-sha256") +HMAC_SHA256_128 = dns.name.from_text("hmac-sha256-128") +HMAC_SHA384 = dns.name.from_text("hmac-sha384") +HMAC_SHA384_192 = dns.name.from_text("hmac-sha384-192") +HMAC_SHA512 = dns.name.from_text("hmac-sha512") +HMAC_SHA512_256 = dns.name.from_text("hmac-sha512-256") +GSS_TSIG = dns.name.from_text("gss-tsig") + +default_algorithm = HMAC_SHA256 + +mac_sizes = { + HMAC_SHA1: 20, + HMAC_SHA224: 28, + HMAC_SHA256: 32, + HMAC_SHA256_128: 16, + HMAC_SHA384: 48, + HMAC_SHA384_192: 24, + HMAC_SHA512: 64, + HMAC_SHA512_256: 32, + HMAC_MD5: 16, + GSS_TSIG: 128, # This is what we assume to be the worst case! +} + + +class GSSTSig: + """ + GSS-TSIG TSIG implementation. This uses the GSS-API context established + in the TKEY message handshake to sign messages using GSS-API message + integrity codes, per the RFC. + + In order to avoid a direct GSSAPI dependency, the keyring holds a ref + to the GSSAPI object required, rather than the key itself. + """ + + def __init__(self, gssapi_context): + self.gssapi_context = gssapi_context + self.data = b"" + self.name = "gss-tsig" + + def update(self, data): + self.data += data + + def sign(self): + # defer to the GSSAPI function to sign + return self.gssapi_context.get_signature(self.data) + + def verify(self, expected): + try: + # defer to the GSSAPI function to verify + return self.gssapi_context.verify_signature(self.data, expected) + except Exception: + # note the usage of a bare exception + raise BadSignature + + +class GSSTSigAdapter: + def __init__(self, keyring): + self.keyring = keyring + + def __call__(self, message, keyname): + if keyname in self.keyring: + key = self.keyring[keyname] + if isinstance(key, Key) and key.algorithm == GSS_TSIG: + if message: + GSSTSigAdapter.parse_tkey_and_step(key, message, keyname) + return key + else: + return None + + @classmethod + def parse_tkey_and_step(cls, key, message, keyname): + # if the message is a TKEY type, absorb the key material + # into the context using step(); this is used to allow the + # client to complete the GSSAPI negotiation before attempting + # to verify the signed response to a TKEY message exchange + try: + rrset = message.find_rrset( + message.answer, keyname, dns.rdataclass.ANY, dns.rdatatype.TKEY + ) + if rrset: + token = rrset[0].key + gssapi_context = key.secret + return gssapi_context.step(token) + except KeyError: + pass + + +class HMACTSig: + """ + HMAC TSIG implementation. This uses the HMAC python module to handle the + sign/verify operations. + """ + + _hashes = { + HMAC_SHA1: hashlib.sha1, + HMAC_SHA224: hashlib.sha224, + HMAC_SHA256: hashlib.sha256, + HMAC_SHA256_128: (hashlib.sha256, 128), + HMAC_SHA384: hashlib.sha384, + HMAC_SHA384_192: (hashlib.sha384, 192), + HMAC_SHA512: hashlib.sha512, + HMAC_SHA512_256: (hashlib.sha512, 256), + HMAC_MD5: hashlib.md5, + } + + def __init__(self, key, algorithm): + try: + hashinfo = self._hashes[algorithm] + except KeyError: + raise NotImplementedError(f"TSIG algorithm {algorithm} is not supported") + + # create the HMAC context + if isinstance(hashinfo, tuple): + self.hmac_context = hmac.new(key, digestmod=hashinfo[0]) + self.size = hashinfo[1] + else: + self.hmac_context = hmac.new(key, digestmod=hashinfo) + self.size = None + self.name = self.hmac_context.name + if self.size: + self.name += f"-{self.size}" + + def update(self, data): + return self.hmac_context.update(data) + + def sign(self): + # defer to the HMAC digest() function for that digestmod + digest = self.hmac_context.digest() + if self.size: + digest = digest[: (self.size // 8)] + return digest + + def verify(self, expected): + # re-digest and compare the results + mac = self.sign() + if not hmac.compare_digest(mac, expected): + raise BadSignature + + +def _digest(wire, key, rdata, time=None, request_mac=None, ctx=None, multi=None): + """Return a context containing the TSIG rdata for the input parameters + @rtype: dns.tsig.HMACTSig or dns.tsig.GSSTSig object + @raises ValueError: I{other_data} is too long + @raises NotImplementedError: I{algorithm} is not supported + """ + + first = not (ctx and multi) + if first: + ctx = get_context(key) + if request_mac: + ctx.update(struct.pack("!H", len(request_mac))) + ctx.update(request_mac) + assert ctx is not None # for type checkers + ctx.update(struct.pack("!H", rdata.original_id)) + ctx.update(wire[2:]) + if first: + ctx.update(key.name.to_digestable()) + ctx.update(struct.pack("!H", dns.rdataclass.ANY)) + ctx.update(struct.pack("!I", 0)) + if time is None: + time = rdata.time_signed + upper_time = (time >> 32) & 0xFFFF + lower_time = time & 0xFFFFFFFF + time_encoded = struct.pack("!HIH", upper_time, lower_time, rdata.fudge) + other_len = len(rdata.other) + if other_len > 65535: + raise ValueError("TSIG Other Data is > 65535 bytes") + if first: + ctx.update(key.algorithm.to_digestable() + time_encoded) + ctx.update(struct.pack("!HH", rdata.error, other_len) + rdata.other) + else: + ctx.update(time_encoded) + return ctx + + +def _maybe_start_digest(key, mac, multi): + """If this is the first message in a multi-message sequence, + start a new context. + @rtype: dns.tsig.HMACTSig or dns.tsig.GSSTSig object + """ + if multi: + ctx = get_context(key) + ctx.update(struct.pack("!H", len(mac))) + ctx.update(mac) + return ctx + else: + return None + + +def sign(wire, key, rdata, time=None, request_mac=None, ctx=None, multi=False): + """Return a (tsig_rdata, mac, ctx) tuple containing the HMAC TSIG rdata + for the input parameters, the HMAC MAC calculated by applying the + TSIG signature algorithm, and the TSIG digest context. + @rtype: (string, dns.tsig.HMACTSig or dns.tsig.GSSTSig object) + @raises ValueError: I{other_data} is too long + @raises NotImplementedError: I{algorithm} is not supported + """ + + ctx = _digest(wire, key, rdata, time, request_mac, ctx, multi) + mac = ctx.sign() + tsig = rdata.replace(time_signed=time, mac=mac) + + return (tsig, _maybe_start_digest(key, mac, multi)) + + +def validate( + wire, key, owner, rdata, now, request_mac, tsig_start, ctx=None, multi=False +): + """Validate the specified TSIG rdata against the other input parameters. + + @raises FormError: The TSIG is badly formed. + @raises BadTime: There is too much time skew between the client and the + server. + @raises BadSignature: The TSIG signature did not validate + @rtype: dns.tsig.HMACTSig or dns.tsig.GSSTSig object""" + + (adcount,) = struct.unpack("!H", wire[10:12]) + if adcount == 0: + raise dns.exception.FormError + adcount -= 1 + new_wire = wire[0:10] + struct.pack("!H", adcount) + wire[12:tsig_start] + if rdata.error != 0: + if rdata.error == dns.rcode.BADSIG: + raise PeerBadSignature + elif rdata.error == dns.rcode.BADKEY: + raise PeerBadKey + elif rdata.error == dns.rcode.BADTIME: + raise PeerBadTime + elif rdata.error == dns.rcode.BADTRUNC: + raise PeerBadTruncation + else: + raise PeerError(f"unknown TSIG error code {rdata.error}") + if abs(rdata.time_signed - now) > rdata.fudge: + raise BadTime + if key.name != owner: + raise BadKey + if key.algorithm != rdata.algorithm: + raise BadAlgorithm + ctx = _digest(new_wire, key, rdata, None, request_mac, ctx, multi) + ctx.verify(rdata.mac) + return _maybe_start_digest(key, rdata.mac, multi) + + +def get_context(key): + """Returns an HMAC context for the specified key. + + @rtype: HMAC context + @raises NotImplementedError: I{algorithm} is not supported + """ + + if key.algorithm == GSS_TSIG: + return GSSTSig(key.secret) + else: + return HMACTSig(key.secret, key.algorithm) + + +class Key: + def __init__( + self, + name: dns.name.Name | str, + secret: bytes | str, + algorithm: dns.name.Name | str = default_algorithm, + ): + if isinstance(name, str): + name = dns.name.from_text(name) + self.name = name + if isinstance(secret, str): + secret = base64.decodebytes(secret.encode()) + self.secret = secret + if isinstance(algorithm, str): + algorithm = dns.name.from_text(algorithm) + self.algorithm = algorithm + + def __eq__(self, other): + return ( + isinstance(other, Key) + and self.name == other.name + and self.secret == other.secret + and self.algorithm == other.algorithm + ) + + def __repr__(self): + r = f" Dict[dns.name.Name, Any]: + """Convert a dictionary containing (textual DNS name, base64 secret) + pairs into a binary keyring which has (dns.name.Name, bytes) pairs, or + a dictionary containing (textual DNS name, (algorithm, base64 secret)) + pairs into a binary keyring which has (dns.name.Name, dns.tsig.Key) pairs. + @rtype: dict""" + + keyring: Dict[dns.name.Name, Any] = {} + for name, value in textring.items(): + kname = dns.name.from_text(name) + if isinstance(value, str): + keyring[kname] = dns.tsig.Key(kname, value).secret + else: + (algorithm, secret) = value + keyring[kname] = dns.tsig.Key(kname, secret, algorithm) + return keyring + + +def to_text(keyring: Dict[dns.name.Name, Any]) -> Dict[str, Any]: + """Convert a dictionary containing (dns.name.Name, dns.tsig.Key) pairs + into a text keyring which has (textual DNS name, (textual algorithm, + base64 secret)) pairs, or a dictionary containing (dns.name.Name, bytes) + pairs into a text keyring which has (textual DNS name, base64 secret) pairs. + @rtype: dict""" + + textring = {} + + def b64encode(secret): + return base64.encodebytes(secret).decode().rstrip() + + for name, key in keyring.items(): + tname = name.to_text() + if isinstance(key, bytes): + textring[tname] = b64encode(key) + else: + if isinstance(key.secret, bytes): + text_secret = b64encode(key.secret) + else: + text_secret = str(key.secret) + + textring[tname] = (key.algorithm.to_text(), text_secret) + return textring diff --git a/netdeploy/lib/python3.11/site-packages/dns/ttl.py b/netdeploy/lib/python3.11/site-packages/dns/ttl.py new file mode 100644 index 0000000..16289cd --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/ttl.py @@ -0,0 +1,90 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS TTL conversion.""" + +import dns.exception + +# Technically TTLs are supposed to be between 0 and 2**31 - 1, with values +# greater than that interpreted as 0, but we do not impose this policy here +# as values > 2**31 - 1 occur in real world data. +# +# We leave it to applications to impose tighter bounds if desired. +MAX_TTL = 2**32 - 1 + + +class BadTTL(dns.exception.SyntaxError): + """DNS TTL value is not well-formed.""" + + +def from_text(text: str) -> int: + """Convert the text form of a TTL to an integer. + + The BIND 8 units syntax for TTLs (e.g. '1w6d4h3m10s') is supported. + + *text*, a ``str``, the textual TTL. + + Raises ``dns.ttl.BadTTL`` if the TTL is not well-formed. + + Returns an ``int``. + """ + + if text.isdigit(): + total = int(text) + elif len(text) == 0: + raise BadTTL + else: + total = 0 + current = 0 + need_digit = True + for c in text: + if c.isdigit(): + current *= 10 + current += int(c) + need_digit = False + else: + if need_digit: + raise BadTTL + c = c.lower() + if c == "w": + total += current * 604800 + elif c == "d": + total += current * 86400 + elif c == "h": + total += current * 3600 + elif c == "m": + total += current * 60 + elif c == "s": + total += current + else: + raise BadTTL(f"unknown unit '{c}'") + current = 0 + need_digit = True + if not current == 0: + raise BadTTL("trailing integer") + if total < 0 or total > MAX_TTL: + raise BadTTL("TTL should be between 0 and 2**32 - 1 (inclusive)") + return total + + +def make(value: int | str) -> int: + if isinstance(value, int): + return value + elif isinstance(value, str): + return from_text(value) + else: + raise ValueError("cannot convert value to TTL") diff --git a/netdeploy/lib/python3.11/site-packages/dns/update.py b/netdeploy/lib/python3.11/site-packages/dns/update.py new file mode 100644 index 0000000..0e4aee4 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/update.py @@ -0,0 +1,389 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Dynamic Update Support""" + +from typing import Any, List + +import dns.enum +import dns.exception +import dns.message +import dns.name +import dns.opcode +import dns.rdata +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.rrset +import dns.tsig + + +class UpdateSection(dns.enum.IntEnum): + """Update sections""" + + ZONE = 0 + PREREQ = 1 + UPDATE = 2 + ADDITIONAL = 3 + + @classmethod + def _maximum(cls): + return 3 + + +class UpdateMessage(dns.message.Message): # lgtm[py/missing-equals] + # ignore the mypy error here as we mean to use a different enum + _section_enum = UpdateSection # type: ignore + + def __init__( + self, + zone: dns.name.Name | str | None = None, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + keyring: Any | None = None, + keyname: dns.name.Name | None = None, + keyalgorithm: dns.name.Name | str = dns.tsig.default_algorithm, + id: int | None = None, + ): + """Initialize a new DNS Update object. + + See the documentation of the Message class for a complete + description of the keyring dictionary. + + *zone*, a ``dns.name.Name``, ``str``, or ``None``, the zone + which is being updated. ``None`` should only be used by dnspython's + message constructors, as a zone is required for the convenience + methods like ``add()``, ``replace()``, etc. + + *rdclass*, an ``int`` or ``str``, the class of the zone. + + The *keyring*, *keyname*, and *keyalgorithm* parameters are passed to + ``use_tsig()``; see its documentation for details. + """ + super().__init__(id=id) + self.flags |= dns.opcode.to_flags(dns.opcode.UPDATE) + if isinstance(zone, str): + zone = dns.name.from_text(zone) + self.origin = zone + rdclass = dns.rdataclass.RdataClass.make(rdclass) + self.zone_rdclass = rdclass + if self.origin: + self.find_rrset( + self.zone, + self.origin, + rdclass, + dns.rdatatype.SOA, + create=True, + force_unique=True, + ) + if keyring is not None: + self.use_tsig(keyring, keyname, algorithm=keyalgorithm) + + @property + def zone(self) -> List[dns.rrset.RRset]: + """The zone section.""" + return self.sections[0] + + @zone.setter + def zone(self, v): + self.sections[0] = v + + @property + def prerequisite(self) -> List[dns.rrset.RRset]: + """The prerequisite section.""" + return self.sections[1] + + @prerequisite.setter + def prerequisite(self, v): + self.sections[1] = v + + @property + def update(self) -> List[dns.rrset.RRset]: + """The update section.""" + return self.sections[2] + + @update.setter + def update(self, v): + self.sections[2] = v + + def _add_rr(self, name, ttl, rd, deleting=None, section=None): + """Add a single RR to the update section.""" + + if section is None: + section = self.update + covers = rd.covers() + rrset = self.find_rrset( + section, name, self.zone_rdclass, rd.rdtype, covers, deleting, True, True + ) + rrset.add(rd, ttl) + + def _add(self, replace, section, name, *args): + """Add records. + + *replace* is the replacement mode. If ``False``, + RRs are added to an existing RRset; if ``True``, the RRset + is replaced with the specified contents. The second + argument is the section to add to. The third argument + is always a name. The other arguments can be: + + - rdataset... + + - ttl, rdata... + + - ttl, rdtype, string... + """ + + if isinstance(name, str): + name = dns.name.from_text(name, None) + if isinstance(args[0], dns.rdataset.Rdataset): + for rds in args: + if replace: + self.delete(name, rds.rdtype) + for rd in rds: + self._add_rr(name, rds.ttl, rd, section=section) + else: + args = list(args) + ttl = int(args.pop(0)) + if isinstance(args[0], dns.rdata.Rdata): + if replace: + self.delete(name, args[0].rdtype) + for rd in args: + self._add_rr(name, ttl, rd, section=section) + else: + rdtype = dns.rdatatype.RdataType.make(args.pop(0)) + if replace: + self.delete(name, rdtype) + for s in args: + rd = dns.rdata.from_text(self.zone_rdclass, rdtype, s, self.origin) + self._add_rr(name, ttl, rd, section=section) + + def add(self, name: dns.name.Name | str, *args: Any) -> None: + """Add records. + + The first argument is always a name. The other + arguments can be: + + - rdataset... + + - ttl, rdata... + + - ttl, rdtype, string... + """ + + self._add(False, self.update, name, *args) + + def delete(self, name: dns.name.Name | str, *args: Any) -> None: + """Delete records. + + The first argument is always a name. The other + arguments can be: + + - *empty* + + - rdataset... + + - rdata... + + - rdtype, [string...] + """ + + if isinstance(name, str): + name = dns.name.from_text(name, None) + if len(args) == 0: + self.find_rrset( + self.update, + name, + dns.rdataclass.ANY, + dns.rdatatype.ANY, + dns.rdatatype.NONE, + dns.rdataclass.ANY, + True, + True, + ) + elif isinstance(args[0], dns.rdataset.Rdataset): + for rds in args: + for rd in rds: + self._add_rr(name, 0, rd, dns.rdataclass.NONE) + else: + largs = list(args) + if isinstance(largs[0], dns.rdata.Rdata): + for rd in largs: + self._add_rr(name, 0, rd, dns.rdataclass.NONE) + else: + rdtype = dns.rdatatype.RdataType.make(largs.pop(0)) + if len(largs) == 0: + self.find_rrset( + self.update, + name, + self.zone_rdclass, + rdtype, + dns.rdatatype.NONE, + dns.rdataclass.ANY, + True, + True, + ) + else: + for s in largs: + rd = dns.rdata.from_text( + self.zone_rdclass, + rdtype, + s, # type: ignore[arg-type] + self.origin, + ) + self._add_rr(name, 0, rd, dns.rdataclass.NONE) + + def replace(self, name: dns.name.Name | str, *args: Any) -> None: + """Replace records. + + The first argument is always a name. The other + arguments can be: + + - rdataset... + + - ttl, rdata... + + - ttl, rdtype, string... + + Note that if you want to replace the entire node, you should do + a delete of the name followed by one or more calls to add. + """ + + self._add(True, self.update, name, *args) + + def present(self, name: dns.name.Name | str, *args: Any) -> None: + """Require that an owner name (and optionally an rdata type, + or specific rdataset) exists as a prerequisite to the + execution of the update. + + The first argument is always a name. + The other arguments can be: + + - rdataset... + + - rdata... + + - rdtype, string... + """ + + if isinstance(name, str): + name = dns.name.from_text(name, None) + if len(args) == 0: + self.find_rrset( + self.prerequisite, + name, + dns.rdataclass.ANY, + dns.rdatatype.ANY, + dns.rdatatype.NONE, + None, + True, + True, + ) + elif ( + isinstance(args[0], dns.rdataset.Rdataset) + or isinstance(args[0], dns.rdata.Rdata) + or len(args) > 1 + ): + if not isinstance(args[0], dns.rdataset.Rdataset): + # Add a 0 TTL + largs = list(args) + largs.insert(0, 0) # type: ignore[arg-type] + self._add(False, self.prerequisite, name, *largs) + else: + self._add(False, self.prerequisite, name, *args) + else: + rdtype = dns.rdatatype.RdataType.make(args[0]) + self.find_rrset( + self.prerequisite, + name, + dns.rdataclass.ANY, + rdtype, + dns.rdatatype.NONE, + None, + True, + True, + ) + + def absent( + self, + name: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str | None = None, + ) -> None: + """Require that an owner name (and optionally an rdata type) does + not exist as a prerequisite to the execution of the update.""" + + if isinstance(name, str): + name = dns.name.from_text(name, None) + if rdtype is None: + self.find_rrset( + self.prerequisite, + name, + dns.rdataclass.NONE, + dns.rdatatype.ANY, + dns.rdatatype.NONE, + None, + True, + True, + ) + else: + rdtype = dns.rdatatype.RdataType.make(rdtype) + self.find_rrset( + self.prerequisite, + name, + dns.rdataclass.NONE, + rdtype, + dns.rdatatype.NONE, + None, + True, + True, + ) + + def _get_one_rr_per_rrset(self, value): + # Updates are always one_rr_per_rrset + return True + + def _parse_rr_header(self, section, name, rdclass, rdtype): # pyright: ignore + deleting = None + empty = False + if section == UpdateSection.ZONE: + if ( + dns.rdataclass.is_metaclass(rdclass) + or rdtype != dns.rdatatype.SOA + or self.zone + ): + raise dns.exception.FormError + else: + if not self.zone: + raise dns.exception.FormError + if rdclass in (dns.rdataclass.ANY, dns.rdataclass.NONE): + deleting = rdclass + rdclass = self.zone[0].rdclass + empty = ( + deleting == dns.rdataclass.ANY or section == UpdateSection.PREREQ + ) + return (rdclass, rdtype, deleting, empty) + + +# backwards compatibility +Update = UpdateMessage + +### BEGIN generated UpdateSection constants + +ZONE = UpdateSection.ZONE +PREREQ = UpdateSection.PREREQ +UPDATE = UpdateSection.UPDATE +ADDITIONAL = UpdateSection.ADDITIONAL + +### END generated UpdateSection constants diff --git a/netdeploy/lib/python3.11/site-packages/dns/version.py b/netdeploy/lib/python3.11/site-packages/dns/version.py new file mode 100644 index 0000000..e11dd29 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/version.py @@ -0,0 +1,42 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""dnspython release version information.""" + +#: MAJOR +MAJOR = 2 +#: MINOR +MINOR = 8 +#: MICRO +MICRO = 0 +#: RELEASELEVEL +RELEASELEVEL = 0x0F +#: SERIAL +SERIAL = 0 + +if RELEASELEVEL == 0x0F: # pragma: no cover lgtm[py/unreachable-statement] + #: version + version = f"{MAJOR}.{MINOR}.{MICRO}" # lgtm[py/unreachable-statement] +elif RELEASELEVEL == 0x00: # pragma: no cover lgtm[py/unreachable-statement] + version = f"{MAJOR}.{MINOR}.{MICRO}dev{SERIAL}" # lgtm[py/unreachable-statement] +elif RELEASELEVEL == 0x0C: # pragma: no cover lgtm[py/unreachable-statement] + version = f"{MAJOR}.{MINOR}.{MICRO}rc{SERIAL}" # lgtm[py/unreachable-statement] +else: # pragma: no cover lgtm[py/unreachable-statement] + version = f"{MAJOR}.{MINOR}.{MICRO}{RELEASELEVEL:x}{SERIAL}" # lgtm[py/unreachable-statement] + +#: hexversion +hexversion = MAJOR << 24 | MINOR << 16 | MICRO << 8 | RELEASELEVEL << 4 | SERIAL diff --git a/netdeploy/lib/python3.11/site-packages/dns/versioned.py b/netdeploy/lib/python3.11/site-packages/dns/versioned.py new file mode 100644 index 0000000..3644711 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/versioned.py @@ -0,0 +1,320 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +"""DNS Versioned Zones.""" + +import collections +import threading +from typing import Callable, Deque, Set, cast + +import dns.exception +import dns.name +import dns.node +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.rdtypes.ANY.SOA +import dns.zone + + +class UseTransaction(dns.exception.DNSException): + """To alter a versioned zone, use a transaction.""" + + +# Backwards compatibility +Node = dns.zone.VersionedNode +ImmutableNode = dns.zone.ImmutableVersionedNode +Version = dns.zone.Version +WritableVersion = dns.zone.WritableVersion +ImmutableVersion = dns.zone.ImmutableVersion +Transaction = dns.zone.Transaction + + +class Zone(dns.zone.Zone): # lgtm[py/missing-equals] + __slots__ = [ + "_versions", + "_versions_lock", + "_write_txn", + "_write_waiters", + "_write_event", + "_pruning_policy", + "_readers", + ] + + node_factory: Callable[[], dns.node.Node] = Node + + def __init__( + self, + origin: dns.name.Name | str | None, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + relativize: bool = True, + pruning_policy: Callable[["Zone", Version], bool | None] | None = None, + ): + """Initialize a versioned zone object. + + *origin* is the origin of the zone. It may be a ``dns.name.Name``, + a ``str``, or ``None``. If ``None``, then the zone's origin will + be set by the first ``$ORIGIN`` line in a zone file. + + *rdclass*, an ``int``, the zone's rdata class; the default is class IN. + + *relativize*, a ``bool``, determine's whether domain names are + relativized to the zone's origin. The default is ``True``. + + *pruning policy*, a function taking a ``Zone`` and a ``Version`` and returning + a ``bool``, or ``None``. Should the version be pruned? If ``None``, + the default policy, which retains one version is used. + """ + super().__init__(origin, rdclass, relativize) + self._versions: Deque[Version] = collections.deque() + self._version_lock = threading.Lock() + if pruning_policy is None: + self._pruning_policy = self._default_pruning_policy + else: + self._pruning_policy = pruning_policy + self._write_txn: Transaction | None = None + self._write_event: threading.Event | None = None + self._write_waiters: Deque[threading.Event] = collections.deque() + self._readers: Set[Transaction] = set() + self._commit_version_unlocked( + None, WritableVersion(self, replacement=True), origin + ) + + def reader( + self, id: int | None = None, serial: int | None = None + ) -> Transaction: # pylint: disable=arguments-differ + if id is not None and serial is not None: + raise ValueError("cannot specify both id and serial") + with self._version_lock: + if id is not None: + version = None + for v in reversed(self._versions): + if v.id == id: + version = v + break + if version is None: + raise KeyError("version not found") + elif serial is not None: + if self.relativize: + oname = dns.name.empty + else: + assert self.origin is not None + oname = self.origin + version = None + for v in reversed(self._versions): + n = v.nodes.get(oname) + if n: + rds = n.get_rdataset(self.rdclass, dns.rdatatype.SOA) + if rds is None: + continue + soa = cast(dns.rdtypes.ANY.SOA.SOA, rds[0]) + if rds and soa.serial == serial: + version = v + break + if version is None: + raise KeyError("serial not found") + else: + version = self._versions[-1] + txn = Transaction(self, False, version) + self._readers.add(txn) + return txn + + def writer(self, replacement: bool = False) -> Transaction: + event = None + while True: + with self._version_lock: + # Checking event == self._write_event ensures that either + # no one was waiting before we got lucky and found no write + # txn, or we were the one who was waiting and got woken up. + # This prevents "taking cuts" when creating a write txn. + if self._write_txn is None and event == self._write_event: + # Creating the transaction defers version setup + # (i.e. copying the nodes dictionary) until we + # give up the lock, so that we hold the lock as + # short a time as possible. This is why we call + # _setup_version() below. + self._write_txn = Transaction( + self, replacement, make_immutable=True + ) + # give up our exclusive right to make a Transaction + self._write_event = None + break + # Someone else is writing already, so we will have to + # wait, but we want to do the actual wait outside the + # lock. + event = threading.Event() + self._write_waiters.append(event) + # wait (note we gave up the lock!) + # + # We only wake one sleeper at a time, so it's important + # that no event waiter can exit this method (e.g. via + # cancellation) without returning a transaction or waking + # someone else up. + # + # This is not a problem with Threading module threads as + # they cannot be canceled, but could be an issue with trio + # tasks when we do the async version of writer(). + # I.e. we'd need to do something like: + # + # try: + # event.wait() + # except trio.Cancelled: + # with self._version_lock: + # self._maybe_wakeup_one_waiter_unlocked() + # raise + # + event.wait() + # Do the deferred version setup. + self._write_txn._setup_version() + return self._write_txn + + def _maybe_wakeup_one_waiter_unlocked(self): + if len(self._write_waiters) > 0: + self._write_event = self._write_waiters.popleft() + self._write_event.set() + + # pylint: disable=unused-argument + def _default_pruning_policy(self, zone, version): + return True + + # pylint: enable=unused-argument + + def _prune_versions_unlocked(self): + assert len(self._versions) > 0 + # Don't ever prune a version greater than or equal to one that + # a reader has open. This pins versions in memory while the + # reader is open, and importantly lets the reader open a txn on + # a successor version (e.g. if generating an IXFR). + # + # Note our definition of least_kept also ensures we do not try to + # delete the greatest version. + if len(self._readers) > 0: + least_kept = min(txn.version.id for txn in self._readers) # pyright: ignore + else: + least_kept = self._versions[-1].id + while self._versions[0].id < least_kept and self._pruning_policy( + self, self._versions[0] + ): + self._versions.popleft() + + def set_max_versions(self, max_versions: int | None) -> None: + """Set a pruning policy that retains up to the specified number + of versions + """ + if max_versions is not None and max_versions < 1: + raise ValueError("max versions must be at least 1") + if max_versions is None: + # pylint: disable=unused-argument + def policy(zone, _): # pyright: ignore + return False + + else: + + def policy(zone, _): + return len(zone._versions) > max_versions + + self.set_pruning_policy(policy) + + def set_pruning_policy( + self, policy: Callable[["Zone", Version], bool | None] | None + ) -> None: + """Set the pruning policy for the zone. + + The *policy* function takes a `Version` and returns `True` if + the version should be pruned, and `False` otherwise. `None` + may also be specified for policy, in which case the default policy + is used. + + Pruning checking proceeds from the least version and the first + time the function returns `False`, the checking stops. I.e. the + retained versions are always a consecutive sequence. + """ + if policy is None: + policy = self._default_pruning_policy + with self._version_lock: + self._pruning_policy = policy + self._prune_versions_unlocked() + + def _end_read(self, txn): + with self._version_lock: + self._readers.remove(txn) + self._prune_versions_unlocked() + + def _end_write_unlocked(self, txn): + assert self._write_txn == txn + self._write_txn = None + self._maybe_wakeup_one_waiter_unlocked() + + def _end_write(self, txn): + with self._version_lock: + self._end_write_unlocked(txn) + + def _commit_version_unlocked(self, txn, version, origin): + self._versions.append(version) + self._prune_versions_unlocked() + self.nodes = version.nodes + if self.origin is None: + self.origin = origin + # txn can be None in __init__ when we make the empty version. + if txn is not None: + self._end_write_unlocked(txn) + + def _commit_version(self, txn, version, origin): + with self._version_lock: + self._commit_version_unlocked(txn, version, origin) + + def _get_next_version_id(self): + if len(self._versions) > 0: + id = self._versions[-1].id + 1 + else: + id = 1 + return id + + def find_node( + self, name: dns.name.Name | str, create: bool = False + ) -> dns.node.Node: + if create: + raise UseTransaction + return super().find_node(name) + + def delete_node(self, name: dns.name.Name | str) -> None: + raise UseTransaction + + def find_rdataset( + self, + name: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset: + if create: + raise UseTransaction + rdataset = super().find_rdataset(name, rdtype, covers) + return dns.rdataset.ImmutableRdataset(rdataset) + + def get_rdataset( + self, + name: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset | None: + if create: + raise UseTransaction + rdataset = super().get_rdataset(name, rdtype, covers) + if rdataset is not None: + return dns.rdataset.ImmutableRdataset(rdataset) + else: + return None + + def delete_rdataset( + self, + name: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + ) -> None: + raise UseTransaction + + def replace_rdataset( + self, name: dns.name.Name | str, replacement: dns.rdataset.Rdataset + ) -> None: + raise UseTransaction diff --git a/netdeploy/lib/python3.11/site-packages/dns/win32util.py b/netdeploy/lib/python3.11/site-packages/dns/win32util.py new file mode 100644 index 0000000..2d77b4c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/win32util.py @@ -0,0 +1,438 @@ +import sys + +import dns._features + +# pylint: disable=W0612,W0613,C0301 + +if sys.platform == "win32": + import ctypes + import ctypes.wintypes as wintypes + import winreg # pylint: disable=import-error + from enum import IntEnum + + import dns.name + + # Keep pylint quiet on non-windows. + try: + _ = WindowsError # pylint: disable=used-before-assignment + except NameError: + WindowsError = Exception + + class ConfigMethod(IntEnum): + Registry = 1 + WMI = 2 + Win32 = 3 + + class DnsInfo: + def __init__(self): + self.domain = None + self.nameservers = [] + self.search = [] + + _config_method = ConfigMethod.Registry + + if dns._features.have("wmi"): + import threading + + import pythoncom # pylint: disable=import-error + import wmi # pylint: disable=import-error + + # Prefer WMI by default if wmi is installed. + _config_method = ConfigMethod.WMI + + class _WMIGetter(threading.Thread): + # pylint: disable=possibly-used-before-assignment + def __init__(self): + super().__init__() + self.info = DnsInfo() + + def run(self): + pythoncom.CoInitialize() + try: + system = wmi.WMI() + for interface in system.Win32_NetworkAdapterConfiguration(): + if interface.IPEnabled and interface.DNSServerSearchOrder: + self.info.nameservers = list(interface.DNSServerSearchOrder) + if interface.DNSDomain: + self.info.domain = _config_domain(interface.DNSDomain) + if interface.DNSDomainSuffixSearchOrder: + self.info.search = [ + _config_domain(x) + for x in interface.DNSDomainSuffixSearchOrder + ] + break + finally: + pythoncom.CoUninitialize() + + def get(self): + # We always run in a separate thread to avoid any issues with + # the COM threading model. + self.start() + self.join() + return self.info + + else: + + class _WMIGetter: # type: ignore + pass + + def _config_domain(domain): + # Sometimes DHCP servers add a '.' prefix to the default domain, and + # Windows just stores such values in the registry (see #687). + # Check for this and fix it. + if domain.startswith("."): + domain = domain[1:] + return dns.name.from_text(domain) + + class _RegistryGetter: + def __init__(self): + self.info = DnsInfo() + + def _split(self, text): + # The windows registry has used both " " and "," as a delimiter, and while + # it is currently using "," in Windows 10 and later, updates can seemingly + # leave a space in too, e.g. "a, b". So we just convert all commas to + # spaces, and use split() in its default configuration, which splits on + # all whitespace and ignores empty strings. + return text.replace(",", " ").split() + + def _config_nameservers(self, nameservers): + for ns in self._split(nameservers): + if ns not in self.info.nameservers: + self.info.nameservers.append(ns) + + def _config_search(self, search): + for s in self._split(search): + s = _config_domain(s) + if s not in self.info.search: + self.info.search.append(s) + + def _config_fromkey(self, key, always_try_domain): + try: + servers, _ = winreg.QueryValueEx(key, "NameServer") + except WindowsError: + servers = None + if servers: + self._config_nameservers(servers) + if servers or always_try_domain: + try: + dom, _ = winreg.QueryValueEx(key, "Domain") + if dom: + self.info.domain = _config_domain(dom) + except WindowsError: + pass + else: + try: + servers, _ = winreg.QueryValueEx(key, "DhcpNameServer") + except WindowsError: + servers = None + if servers: + self._config_nameservers(servers) + try: + dom, _ = winreg.QueryValueEx(key, "DhcpDomain") + if dom: + self.info.domain = _config_domain(dom) + except WindowsError: + pass + try: + search, _ = winreg.QueryValueEx(key, "SearchList") + except WindowsError: + search = None + if search is None: + try: + search, _ = winreg.QueryValueEx(key, "DhcpSearchList") + except WindowsError: + search = None + if search: + self._config_search(search) + + def _is_nic_enabled(self, lm, guid): + # Look in the Windows Registry to determine whether the network + # interface corresponding to the given guid is enabled. + # + # (Code contributed by Paul Marks, thanks!) + # + try: + # This hard-coded location seems to be consistent, at least + # from Windows 2000 through Vista. + connection_key = winreg.OpenKey( + lm, + r"SYSTEM\CurrentControlSet\Control\Network" + r"\{4D36E972-E325-11CE-BFC1-08002BE10318}" + rf"\{guid}\Connection", + ) + + try: + # The PnpInstanceID points to a key inside Enum + (pnp_id, ttype) = winreg.QueryValueEx( + connection_key, "PnpInstanceID" + ) + + if ttype != winreg.REG_SZ: + raise ValueError # pragma: no cover + + device_key = winreg.OpenKey( + lm, rf"SYSTEM\CurrentControlSet\Enum\{pnp_id}" + ) + + try: + # Get ConfigFlags for this device + (flags, ttype) = winreg.QueryValueEx(device_key, "ConfigFlags") + + if ttype != winreg.REG_DWORD: + raise ValueError # pragma: no cover + + # Based on experimentation, bit 0x1 indicates that the + # device is disabled. + # + # XXXRTH I suspect we really want to & with 0x03 so + # that CONFIGFLAGS_REMOVED devices are also ignored, + # but we're shifting to WMI as ConfigFlags is not + # supposed to be used. + return not flags & 0x1 + + finally: + device_key.Close() + finally: + connection_key.Close() + except Exception: # pragma: no cover + return False + + def get(self): + """Extract resolver configuration from the Windows registry.""" + + lm = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + try: + tcp_params = winreg.OpenKey( + lm, r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" + ) + try: + self._config_fromkey(tcp_params, True) + finally: + tcp_params.Close() + interfaces = winreg.OpenKey( + lm, + r"SYSTEM\CurrentControlSet\Services\Tcpip\Parameters\Interfaces", + ) + try: + i = 0 + while True: + try: + guid = winreg.EnumKey(interfaces, i) + i += 1 + key = winreg.OpenKey(interfaces, guid) + try: + if not self._is_nic_enabled(lm, guid): + continue + self._config_fromkey(key, False) + finally: + key.Close() + except OSError: + break + finally: + interfaces.Close() + finally: + lm.Close() + return self.info + + class _Win32Getter(_RegistryGetter): + + def get(self): + """Get the attributes using the Windows API.""" + # Load the IP Helper library + # # https://learn.microsoft.com/en-us/windows/win32/api/iphlpapi/nf-iphlpapi-getadaptersaddresses + IPHLPAPI = ctypes.WinDLL("Iphlpapi.dll") + + # Constants + AF_UNSPEC = 0 + ERROR_SUCCESS = 0 + GAA_FLAG_INCLUDE_PREFIX = 0x00000010 + AF_INET = 2 + AF_INET6 = 23 + IF_TYPE_SOFTWARE_LOOPBACK = 24 + + # Define necessary structures + class SOCKADDRV4(ctypes.Structure): + _fields_ = [ + ("sa_family", wintypes.USHORT), + ("sa_data", ctypes.c_ubyte * 14), + ] + + class SOCKADDRV6(ctypes.Structure): + _fields_ = [ + ("sa_family", wintypes.USHORT), + ("sa_data", ctypes.c_ubyte * 26), + ] + + class SOCKET_ADDRESS(ctypes.Structure): + _fields_ = [ + ("lpSockaddr", ctypes.POINTER(SOCKADDRV4)), + ("iSockaddrLength", wintypes.INT), + ] + + class IP_ADAPTER_DNS_SERVER_ADDRESS(ctypes.Structure): + pass # Forward declaration + + IP_ADAPTER_DNS_SERVER_ADDRESS._fields_ = [ + ("Length", wintypes.ULONG), + ("Reserved", wintypes.DWORD), + ("Next", ctypes.POINTER(IP_ADAPTER_DNS_SERVER_ADDRESS)), + ("Address", SOCKET_ADDRESS), + ] + + class IF_LUID(ctypes.Structure): + _fields_ = [("Value", ctypes.c_ulonglong)] + + class NET_IF_NETWORK_GUID(ctypes.Structure): + _fields_ = [("Value", ctypes.c_ubyte * 16)] + + class IP_ADAPTER_PREFIX_XP(ctypes.Structure): + pass # Left undefined here for simplicity + + class IP_ADAPTER_GATEWAY_ADDRESS_LH(ctypes.Structure): + pass # Left undefined here for simplicity + + class IP_ADAPTER_DNS_SUFFIX(ctypes.Structure): + _fields_ = [ + ("String", ctypes.c_wchar * 256), + ("Next", ctypes.POINTER(ctypes.c_void_p)), + ] + + class IP_ADAPTER_UNICAST_ADDRESS_LH(ctypes.Structure): + pass # Left undefined here for simplicity + + class IP_ADAPTER_MULTICAST_ADDRESS_XP(ctypes.Structure): + pass # Left undefined here for simplicity + + class IP_ADAPTER_ANYCAST_ADDRESS_XP(ctypes.Structure): + pass # Left undefined here for simplicity + + class IP_ADAPTER_DNS_SERVER_ADDRESS_XP(ctypes.Structure): + pass # Left undefined here for simplicity + + class IP_ADAPTER_ADDRESSES(ctypes.Structure): + pass # Forward declaration + + IP_ADAPTER_ADDRESSES._fields_ = [ + ("Length", wintypes.ULONG), + ("IfIndex", wintypes.DWORD), + ("Next", ctypes.POINTER(IP_ADAPTER_ADDRESSES)), + ("AdapterName", ctypes.c_char_p), + ("FirstUnicastAddress", ctypes.POINTER(SOCKET_ADDRESS)), + ("FirstAnycastAddress", ctypes.POINTER(SOCKET_ADDRESS)), + ("FirstMulticastAddress", ctypes.POINTER(SOCKET_ADDRESS)), + ( + "FirstDnsServerAddress", + ctypes.POINTER(IP_ADAPTER_DNS_SERVER_ADDRESS), + ), + ("DnsSuffix", wintypes.LPWSTR), + ("Description", wintypes.LPWSTR), + ("FriendlyName", wintypes.LPWSTR), + ("PhysicalAddress", ctypes.c_ubyte * 8), + ("PhysicalAddressLength", wintypes.ULONG), + ("Flags", wintypes.ULONG), + ("Mtu", wintypes.ULONG), + ("IfType", wintypes.ULONG), + ("OperStatus", ctypes.c_uint), + # Remaining fields removed for brevity + ] + + def format_ipv4(sockaddr_in): + return ".".join(map(str, sockaddr_in.sa_data[2:6])) + + def format_ipv6(sockaddr_in6): + # The sa_data is: + # + # USHORT sin6_port; + # ULONG sin6_flowinfo; + # IN6_ADDR sin6_addr; + # ULONG sin6_scope_id; + # + # which is 2 + 4 + 16 + 4 = 26 bytes, and we need the plus 6 below + # to be in the sin6_addr range. + parts = [ + sockaddr_in6.sa_data[i + 6] << 8 | sockaddr_in6.sa_data[i + 6 + 1] + for i in range(0, 16, 2) + ] + return ":".join(f"{part:04x}" for part in parts) + + buffer_size = ctypes.c_ulong(15000) + while True: + buffer = ctypes.create_string_buffer(buffer_size.value) + + ret_val = IPHLPAPI.GetAdaptersAddresses( + AF_UNSPEC, + GAA_FLAG_INCLUDE_PREFIX, + None, + buffer, + ctypes.byref(buffer_size), + ) + + if ret_val == ERROR_SUCCESS: + break + elif ret_val != 0x6F: # ERROR_BUFFER_OVERFLOW + print(f"Error retrieving adapter information: {ret_val}") + return + + adapter_addresses = ctypes.cast( + buffer, ctypes.POINTER(IP_ADAPTER_ADDRESSES) + ) + + current_adapter = adapter_addresses + while current_adapter: + + # Skip non-operational adapters. + oper_status = current_adapter.contents.OperStatus + if oper_status != 1: + current_adapter = current_adapter.contents.Next + continue + + # Exclude loopback adapters. + if current_adapter.contents.IfType == IF_TYPE_SOFTWARE_LOOPBACK: + current_adapter = current_adapter.contents.Next + continue + + # Get the domain from the DnsSuffix attribute. + dns_suffix = current_adapter.contents.DnsSuffix + if dns_suffix: + self.info.domain = dns.name.from_text(dns_suffix) + + current_dns_server = current_adapter.contents.FirstDnsServerAddress + while current_dns_server: + sockaddr = current_dns_server.contents.Address.lpSockaddr + sockaddr_family = sockaddr.contents.sa_family + + ip = None + if sockaddr_family == AF_INET: # IPv4 + ip = format_ipv4(sockaddr.contents) + elif sockaddr_family == AF_INET6: # IPv6 + sockaddr = ctypes.cast(sockaddr, ctypes.POINTER(SOCKADDRV6)) + ip = format_ipv6(sockaddr.contents) + + if ip: + if ip not in self.info.nameservers: + self.info.nameservers.append(ip) + + current_dns_server = current_dns_server.contents.Next + + current_adapter = current_adapter.contents.Next + + # Use the registry getter to get the search info, since it is set at the system level. + registry_getter = _RegistryGetter() + info = registry_getter.get() + self.info.search = info.search + return self.info + + def set_config_method(method: ConfigMethod) -> None: + global _config_method + _config_method = method + + def get_dns_info() -> DnsInfo: + """Extract resolver configuration.""" + if _config_method == ConfigMethod.Win32: + getter = _Win32Getter() + elif _config_method == ConfigMethod.WMI: + getter = _WMIGetter() + else: + getter = _RegistryGetter() + return getter.get() diff --git a/netdeploy/lib/python3.11/site-packages/dns/wire.py b/netdeploy/lib/python3.11/site-packages/dns/wire.py new file mode 100644 index 0000000..cd027fa --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/wire.py @@ -0,0 +1,98 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +import contextlib +import struct +from typing import Iterator, Optional, Tuple + +import dns.exception +import dns.name + + +class Parser: + """Helper class for parsing DNS wire format.""" + + def __init__(self, wire: bytes, current: int = 0): + """Initialize a Parser + + *wire*, a ``bytes`` contains the data to be parsed, and possibly other data. + Typically it is the whole message or a slice of it. + + *current*, an `int`, the offset within *wire* where parsing should begin. + """ + self.wire = wire + self.current = 0 + self.end = len(self.wire) + if current: + self.seek(current) + self.furthest = current + + def remaining(self) -> int: + return self.end - self.current + + def get_bytes(self, size: int) -> bytes: + assert size >= 0 + if size > self.remaining(): + raise dns.exception.FormError + output = self.wire[self.current : self.current + size] + self.current += size + self.furthest = max(self.furthest, self.current) + return output + + def get_counted_bytes(self, length_size: int = 1) -> bytes: + length = int.from_bytes(self.get_bytes(length_size), "big") + return self.get_bytes(length) + + def get_remaining(self) -> bytes: + return self.get_bytes(self.remaining()) + + def get_uint8(self) -> int: + return struct.unpack("!B", self.get_bytes(1))[0] + + def get_uint16(self) -> int: + return struct.unpack("!H", self.get_bytes(2))[0] + + def get_uint32(self) -> int: + return struct.unpack("!I", self.get_bytes(4))[0] + + def get_uint48(self) -> int: + return int.from_bytes(self.get_bytes(6), "big") + + def get_struct(self, format: str) -> Tuple: + return struct.unpack(format, self.get_bytes(struct.calcsize(format))) + + def get_name(self, origin: Optional["dns.name.Name"] = None) -> "dns.name.Name": + name = dns.name.from_wire_parser(self) + if origin: + name = name.relativize(origin) + return name + + def seek(self, where: int) -> None: + # Note that seeking to the end is OK! (If you try to read + # after such a seek, you'll get an exception as expected.) + if where < 0 or where > self.end: + raise dns.exception.FormError + self.current = where + + @contextlib.contextmanager + def restrict_to(self, size: int) -> Iterator: + assert size >= 0 + if size > self.remaining(): + raise dns.exception.FormError + saved_end = self.end + try: + self.end = self.current + size + yield + # We make this check here and not in the finally as we + # don't want to raise if we're already raising for some + # other reason. + if self.current != self.end: + raise dns.exception.FormError + finally: + self.end = saved_end + + @contextlib.contextmanager + def restore_furthest(self) -> Iterator: + try: + yield None + finally: + self.current = self.furthest diff --git a/netdeploy/lib/python3.11/site-packages/dns/xfr.py b/netdeploy/lib/python3.11/site-packages/dns/xfr.py new file mode 100644 index 0000000..219fdc8 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/xfr.py @@ -0,0 +1,356 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2017 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +from typing import Any, List, Tuple, cast + +import dns.edns +import dns.exception +import dns.message +import dns.name +import dns.rcode +import dns.rdata +import dns.rdataset +import dns.rdatatype +import dns.rdtypes +import dns.rdtypes.ANY +import dns.rdtypes.ANY.SMIMEA +import dns.rdtypes.ANY.SOA +import dns.rdtypes.svcbbase +import dns.serial +import dns.transaction +import dns.tsig +import dns.zone + + +class TransferError(dns.exception.DNSException): + """A zone transfer response got a non-zero rcode.""" + + def __init__(self, rcode): + message = f"Zone transfer error: {dns.rcode.to_text(rcode)}" + super().__init__(message) + self.rcode = rcode + + +class SerialWentBackwards(dns.exception.FormError): + """The current serial number is less than the serial we know.""" + + +class UseTCP(dns.exception.DNSException): + """This IXFR cannot be completed with UDP.""" + + +class Inbound: + """ + State machine for zone transfers. + """ + + def __init__( + self, + txn_manager: dns.transaction.TransactionManager, + rdtype: dns.rdatatype.RdataType = dns.rdatatype.AXFR, + serial: int | None = None, + is_udp: bool = False, + ): + """Initialize an inbound zone transfer. + + *txn_manager* is a :py:class:`dns.transaction.TransactionManager`. + + *rdtype* can be `dns.rdatatype.AXFR` or `dns.rdatatype.IXFR` + + *serial* is the base serial number for IXFRs, and is required in + that case. + + *is_udp*, a ``bool`` indidicates if UDP is being used for this + XFR. + """ + self.txn_manager = txn_manager + self.txn: dns.transaction.Transaction | None = None + self.rdtype = rdtype + if rdtype == dns.rdatatype.IXFR: + if serial is None: + raise ValueError("a starting serial must be supplied for IXFRs") + self.incremental = True + elif rdtype == dns.rdatatype.AXFR: + if is_udp: + raise ValueError("is_udp specified for AXFR") + self.incremental = False + else: + raise ValueError("rdtype is not IXFR or AXFR") + self.serial = serial + self.is_udp = is_udp + (_, _, self.origin) = txn_manager.origin_information() + self.soa_rdataset: dns.rdataset.Rdataset | None = None + self.done = False + self.expecting_SOA = False + self.delete_mode = False + + def process_message(self, message: dns.message.Message) -> bool: + """Process one message in the transfer. + + The message should have the same relativization as was specified when + the `dns.xfr.Inbound` was created. The message should also have been + created with `one_rr_per_rrset=True` because order matters. + + Returns `True` if the transfer is complete, and `False` otherwise. + """ + if self.txn is None: + self.txn = self.txn_manager.writer(not self.incremental) + rcode = message.rcode() + if rcode != dns.rcode.NOERROR: + raise TransferError(rcode) + # + # We don't require a question section, but if it is present is + # should be correct. + # + if len(message.question) > 0: + if message.question[0].name != self.origin: + raise dns.exception.FormError("wrong question name") + if message.question[0].rdtype != self.rdtype: + raise dns.exception.FormError("wrong question rdatatype") + answer_index = 0 + if self.soa_rdataset is None: + # + # This is the first message. We're expecting an SOA at + # the origin. + # + if not message.answer or message.answer[0].name != self.origin: + raise dns.exception.FormError("No answer or RRset not for zone origin") + rrset = message.answer[0] + rdataset = rrset + if rdataset.rdtype != dns.rdatatype.SOA: + raise dns.exception.FormError("first RRset is not an SOA") + answer_index = 1 + self.soa_rdataset = rdataset.copy() # pyright: ignore + if self.incremental: + assert self.soa_rdataset is not None + soa = cast(dns.rdtypes.ANY.SOA.SOA, self.soa_rdataset[0]) + if soa.serial == self.serial: + # + # We're already up-to-date. + # + self.done = True + elif dns.serial.Serial(soa.serial) < self.serial: + # It went backwards! + raise SerialWentBackwards + else: + if self.is_udp and len(message.answer[answer_index:]) == 0: + # + # There are no more records, so this is the + # "truncated" response. Say to use TCP + # + raise UseTCP + # + # Note we're expecting another SOA so we can detect + # if this IXFR response is an AXFR-style response. + # + self.expecting_SOA = True + # + # Process the answer section (other than the initial SOA in + # the first message). + # + for rrset in message.answer[answer_index:]: + name = rrset.name + rdataset = rrset + if self.done: + raise dns.exception.FormError("answers after final SOA") + assert self.txn is not None # for mypy + if rdataset.rdtype == dns.rdatatype.SOA and name == self.origin: + # + # Every time we see an origin SOA delete_mode inverts + # + if self.incremental: + self.delete_mode = not self.delete_mode + # + # If this SOA Rdataset is equal to the first we saw + # then we're finished. If this is an IXFR we also + # check that we're seeing the record in the expected + # part of the response. + # + if rdataset == self.soa_rdataset and ( + (not self.incremental) or self.delete_mode + ): + # + # This is the final SOA + # + soa = cast(dns.rdtypes.ANY.SOA.SOA, rdataset[0]) + if self.expecting_SOA: + # We got an empty IXFR sequence! + raise dns.exception.FormError("empty IXFR sequence") + if self.incremental and self.serial != soa.serial: + raise dns.exception.FormError("unexpected end of IXFR sequence") + self.txn.replace(name, rdataset) + self.txn.commit() + self.txn = None + self.done = True + else: + # + # This is not the final SOA + # + self.expecting_SOA = False + soa = cast(dns.rdtypes.ANY.SOA.SOA, rdataset[0]) + if self.incremental: + if self.delete_mode: + # This is the start of an IXFR deletion set + if soa.serial != self.serial: + raise dns.exception.FormError( + "IXFR base serial mismatch" + ) + else: + # This is the start of an IXFR addition set + self.serial = soa.serial + self.txn.replace(name, rdataset) + else: + # We saw a non-final SOA for the origin in an AXFR. + raise dns.exception.FormError("unexpected origin SOA in AXFR") + continue + if self.expecting_SOA: + # + # We made an IXFR request and are expecting another + # SOA RR, but saw something else, so this must be an + # AXFR response. + # + self.incremental = False + self.expecting_SOA = False + self.delete_mode = False + self.txn.rollback() + self.txn = self.txn_manager.writer(True) + # + # Note we are falling through into the code below + # so whatever rdataset this was gets written. + # + # Add or remove the data + if self.delete_mode: + self.txn.delete_exact(name, rdataset) + else: + self.txn.add(name, rdataset) + if self.is_udp and not self.done: + # + # This is a UDP IXFR and we didn't get to done, and we didn't + # get the proper "truncated" response + # + raise dns.exception.FormError("unexpected end of UDP IXFR") + return self.done + + # + # Inbounds are context managers. + # + + def __enter__(self): + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + if self.txn: + self.txn.rollback() + return False + + +def make_query( + txn_manager: dns.transaction.TransactionManager, + serial: int | None = 0, + use_edns: int | bool | None = None, + ednsflags: int | None = None, + payload: int | None = None, + request_payload: int | None = None, + options: List[dns.edns.Option] | None = None, + keyring: Any = None, + keyname: dns.name.Name | None = None, + keyalgorithm: dns.name.Name | str = dns.tsig.default_algorithm, +) -> Tuple[dns.message.QueryMessage, int | None]: + """Make an AXFR or IXFR query. + + *txn_manager* is a ``dns.transaction.TransactionManager``, typically a + ``dns.zone.Zone``. + + *serial* is an ``int`` or ``None``. If 0, then IXFR will be + attempted using the most recent serial number from the + *txn_manager*; it is the caller's responsibility to ensure there + are no write transactions active that could invalidate the + retrieved serial. If a serial cannot be determined, AXFR will be + forced. Other integer values are the starting serial to use. + ``None`` forces an AXFR. + + Please see the documentation for :py:func:`dns.message.make_query` and + :py:func:`dns.message.Message.use_tsig` for details on the other parameters + to this function. + + Returns a `(query, serial)` tuple. + """ + (zone_origin, _, origin) = txn_manager.origin_information() + if zone_origin is None: + raise ValueError("no zone origin") + if serial is None: + rdtype = dns.rdatatype.AXFR + elif not isinstance(serial, int): + raise ValueError("serial is not an integer") + elif serial == 0: + with txn_manager.reader() as txn: + rdataset = txn.get(origin, "SOA") + if rdataset: + soa = cast(dns.rdtypes.ANY.SOA.SOA, rdataset[0]) + serial = soa.serial + rdtype = dns.rdatatype.IXFR + else: + serial = None + rdtype = dns.rdatatype.AXFR + elif serial > 0 and serial < 4294967296: + rdtype = dns.rdatatype.IXFR + else: + raise ValueError("serial out-of-range") + rdclass = txn_manager.get_class() + q = dns.message.make_query( + zone_origin, + rdtype, + rdclass, + use_edns, + False, + ednsflags, + payload, + request_payload, + options, + ) + if serial is not None: + rdata = dns.rdata.from_text(rdclass, "SOA", f". . {serial} 0 0 0 0") + rrset = q.find_rrset( + q.authority, zone_origin, rdclass, dns.rdatatype.SOA, create=True + ) + rrset.add(rdata, 0) + if keyring is not None: + q.use_tsig(keyring, keyname, algorithm=keyalgorithm) + return (q, serial) + + +def extract_serial_from_query(query: dns.message.Message) -> int | None: + """Extract the SOA serial number from query if it is an IXFR and return + it, otherwise return None. + + *query* is a dns.message.QueryMessage that is an IXFR or AXFR request. + + Raises if the query is not an IXFR or AXFR, or if an IXFR doesn't have + an appropriate SOA RRset in the authority section. + """ + if not isinstance(query, dns.message.QueryMessage): + raise ValueError("query not a QueryMessage") + question = query.question[0] + if question.rdtype == dns.rdatatype.AXFR: + return None + elif question.rdtype != dns.rdatatype.IXFR: + raise ValueError("query is not an AXFR or IXFR") + soa_rrset = query.find_rrset( + query.authority, question.name, question.rdclass, dns.rdatatype.SOA + ) + soa = cast(dns.rdtypes.ANY.SOA.SOA, soa_rrset[0]) + return soa.serial diff --git a/netdeploy/lib/python3.11/site-packages/dns/zone.py b/netdeploy/lib/python3.11/site-packages/dns/zone.py new file mode 100644 index 0000000..f916ffe --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/zone.py @@ -0,0 +1,1462 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Zones.""" + +import contextlib +import io +import os +import struct +from typing import ( + Any, + Callable, + Iterable, + Iterator, + List, + MutableMapping, + Set, + Tuple, + cast, +) + +import dns.exception +import dns.grange +import dns.immutable +import dns.name +import dns.node +import dns.rdata +import dns.rdataclass +import dns.rdataset +import dns.rdatatype +import dns.rdtypes.ANY.SOA +import dns.rdtypes.ANY.ZONEMD +import dns.rrset +import dns.tokenizer +import dns.transaction +import dns.ttl +import dns.zonefile +from dns.zonetypes import DigestHashAlgorithm, DigestScheme, _digest_hashers + + +class BadZone(dns.exception.DNSException): + """The DNS zone is malformed.""" + + +class NoSOA(BadZone): + """The DNS zone has no SOA RR at its origin.""" + + +class NoNS(BadZone): + """The DNS zone has no NS RRset at its origin.""" + + +class UnknownOrigin(BadZone): + """The DNS zone's origin is unknown.""" + + +class UnsupportedDigestScheme(dns.exception.DNSException): + """The zone digest's scheme is unsupported.""" + + +class UnsupportedDigestHashAlgorithm(dns.exception.DNSException): + """The zone digest's origin is unsupported.""" + + +class NoDigest(dns.exception.DNSException): + """The DNS zone has no ZONEMD RRset at its origin.""" + + +class DigestVerificationFailure(dns.exception.DNSException): + """The ZONEMD digest failed to verify.""" + + +def _validate_name( + name: dns.name.Name, + origin: dns.name.Name | None, + relativize: bool, +) -> dns.name.Name: + # This name validation code is shared by Zone and Version + if origin is None: + # This should probably never happen as other code (e.g. + # _rr_line) will notice the lack of an origin before us, but + # we check just in case! + raise KeyError("no zone origin is defined") + if name.is_absolute(): + if not name.is_subdomain(origin): + raise KeyError("name parameter must be a subdomain of the zone origin") + if relativize: + name = name.relativize(origin) + else: + # We have a relative name. Make sure that the derelativized name is + # not too long. + try: + abs_name = name.derelativize(origin) + except dns.name.NameTooLong: + # We map dns.name.NameTooLong to KeyError to be consistent with + # the other exceptions above. + raise KeyError("relative name too long for zone") + if not relativize: + # We have a relative name in a non-relative zone, so use the + # derelativized name. + name = abs_name + return name + + +class Zone(dns.transaction.TransactionManager): + """A DNS zone. + + A ``Zone`` is a mapping from names to nodes. The zone object may be + treated like a Python dictionary, e.g. ``zone[name]`` will retrieve + the node associated with that name. The *name* may be a + ``dns.name.Name object``, or it may be a string. In either case, + if the name is relative it is treated as relative to the origin of + the zone. + """ + + node_factory: Callable[[], dns.node.Node] = dns.node.Node + map_factory: Callable[[], MutableMapping[dns.name.Name, dns.node.Node]] = dict + # We only require the version types as "Version" to allow for flexibility, as + # only the version protocol matters + writable_version_factory: Callable[["Zone", bool], "Version"] | None = None + immutable_version_factory: Callable[["Version"], "Version"] | None = None + + __slots__ = ["rdclass", "origin", "nodes", "relativize"] + + def __init__( + self, + origin: dns.name.Name | str | None, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + relativize: bool = True, + ): + """Initialize a zone object. + + *origin* is the origin of the zone. It may be a ``dns.name.Name``, + a ``str``, or ``None``. If ``None``, then the zone's origin will + be set by the first ``$ORIGIN`` line in a zone file. + + *rdclass*, an ``int``, the zone's rdata class; the default is class IN. + + *relativize*, a ``bool``, determine's whether domain names are + relativized to the zone's origin. The default is ``True``. + """ + + if origin is not None: + if isinstance(origin, str): + origin = dns.name.from_text(origin) + elif not isinstance(origin, dns.name.Name): + raise ValueError("origin parameter must be convertible to a DNS name") + if not origin.is_absolute(): + raise ValueError("origin parameter must be an absolute name") + self.origin = origin + self.rdclass = rdclass + self.nodes: MutableMapping[dns.name.Name, dns.node.Node] = self.map_factory() + self.relativize = relativize + + def __eq__(self, other): + """Two zones are equal if they have the same origin, class, and + nodes. + + Returns a ``bool``. + """ + + if not isinstance(other, Zone): + return False + if ( + self.rdclass != other.rdclass + or self.origin != other.origin + or self.nodes != other.nodes + ): + return False + return True + + def __ne__(self, other): + """Are two zones not equal? + + Returns a ``bool``. + """ + + return not self.__eq__(other) + + def _validate_name(self, name: dns.name.Name | str) -> dns.name.Name: + # Note that any changes in this method should have corresponding changes + # made in the Version _validate_name() method. + if isinstance(name, str): + name = dns.name.from_text(name, None) + elif not isinstance(name, dns.name.Name): + raise KeyError("name parameter must be convertible to a DNS name") + return _validate_name(name, self.origin, self.relativize) + + def __getitem__(self, key): + key = self._validate_name(key) + return self.nodes[key] + + def __setitem__(self, key, value): + key = self._validate_name(key) + self.nodes[key] = value + + def __delitem__(self, key): + key = self._validate_name(key) + del self.nodes[key] + + def __iter__(self): + return self.nodes.__iter__() + + def keys(self): + return self.nodes.keys() + + def values(self): + return self.nodes.values() + + def items(self): + return self.nodes.items() + + def get(self, key): + key = self._validate_name(key) + return self.nodes.get(key) + + def __contains__(self, key): + key = self._validate_name(key) + return key in self.nodes + + def find_node( + self, name: dns.name.Name | str, create: bool = False + ) -> dns.node.Node: + """Find a node in the zone, possibly creating it. + + *name*: the name of the node to find. + The value may be a ``dns.name.Name`` or a ``str``. If absolute, the + name must be a subdomain of the zone's origin. If ``zone.relativize`` + is ``True``, then the name will be relativized. + + *create*, a ``bool``. If true, the node will be created if it does + not exist. + + Raises ``KeyError`` if the name is not known and create was + not specified, or if the name was not a subdomain of the origin. + + Returns a ``dns.node.Node``. + """ + + name = self._validate_name(name) + node = self.nodes.get(name) + if node is None: + if not create: + raise KeyError + node = self.node_factory() + self.nodes[name] = node + return node + + def get_node( + self, name: dns.name.Name | str, create: bool = False + ) -> dns.node.Node | None: + """Get a node in the zone, possibly creating it. + + This method is like ``find_node()``, except it returns None instead + of raising an exception if the node does not exist and creation + has not been requested. + + *name*: the name of the node to find. + The value may be a ``dns.name.Name`` or a ``str``. If absolute, the + name must be a subdomain of the zone's origin. If ``zone.relativize`` + is ``True``, then the name will be relativized. + + *create*, a ``bool``. If true, the node will be created if it does + not exist. + + Returns a ``dns.node.Node`` or ``None``. + """ + + try: + node = self.find_node(name, create) + except KeyError: + node = None + return node + + def delete_node(self, name: dns.name.Name | str) -> None: + """Delete the specified node if it exists. + + *name*: the name of the node to find. + The value may be a ``dns.name.Name`` or a ``str``. If absolute, the + name must be a subdomain of the zone's origin. If ``zone.relativize`` + is ``True``, then the name will be relativized. + + It is not an error if the node does not exist. + """ + + name = self._validate_name(name) + if name in self.nodes: + del self.nodes[name] + + def find_rdataset( + self, + name: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset: + """Look for an rdataset with the specified name and type in the zone, + and return an rdataset encapsulating it. + + The rdataset returned is not a copy; changes to it will change + the zone. + + KeyError is raised if the name or type are not found. + + *name*: the name of the node to find. + The value may be a ``dns.name.Name`` or a ``str``. If absolute, the + name must be a subdomain of the zone's origin. If ``zone.relativize`` + is ``True``, then the name will be relativized. + + *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired. + + *covers*, a ``dns.rdatatype.RdataType`` or ``str`` the covered type. + Usually this value is ``dns.rdatatype.NONE``, but if the + rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, + then the covers value will be the rdata type the SIG/RRSIG + covers. The library treats the SIG and RRSIG types as if they + were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). + This makes RRSIGs much easier to work with than if RRSIGs + covering different rdata types were aggregated into a single + RRSIG rdataset. + + *create*, a ``bool``. If true, the node will be created if it does + not exist. + + Raises ``KeyError`` if the name is not known and create was + not specified, or if the name was not a subdomain of the origin. + + Returns a ``dns.rdataset.Rdataset``. + """ + + name = self._validate_name(name) + rdtype = dns.rdatatype.RdataType.make(rdtype) + covers = dns.rdatatype.RdataType.make(covers) + node = self.find_node(name, create) + return node.find_rdataset(self.rdclass, rdtype, covers, create) + + def get_rdataset( + self, + name: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset | None: + """Look for an rdataset with the specified name and type in the zone. + + This method is like ``find_rdataset()``, except it returns None instead + of raising an exception if the rdataset does not exist and creation + has not been requested. + + The rdataset returned is not a copy; changes to it will change + the zone. + + *name*: the name of the node to find. + The value may be a ``dns.name.Name`` or a ``str``. If absolute, the + name must be a subdomain of the zone's origin. If ``zone.relativize`` + is ``True``, then the name will be relativized. + + *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired. + + *covers*, a ``dns.rdatatype.RdataType`` or ``str``, the covered type. + Usually this value is ``dns.rdatatype.NONE``, but if the + rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, + then the covers value will be the rdata type the SIG/RRSIG + covers. The library treats the SIG and RRSIG types as if they + were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). + This makes RRSIGs much easier to work with than if RRSIGs + covering different rdata types were aggregated into a single + RRSIG rdataset. + + *create*, a ``bool``. If true, the node will be created if it does + not exist. + + Raises ``KeyError`` if the name is not known and create was + not specified, or if the name was not a subdomain of the origin. + + Returns a ``dns.rdataset.Rdataset`` or ``None``. + """ + + try: + rdataset = self.find_rdataset(name, rdtype, covers, create) + except KeyError: + rdataset = None + return rdataset + + def delete_rdataset( + self, + name: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + ) -> None: + """Delete the rdataset matching *rdtype* and *covers*, if it + exists at the node specified by *name*. + + It is not an error if the node does not exist, or if there is no matching + rdataset at the node. + + If the node has no rdatasets after the deletion, it will itself be deleted. + + *name*: the name of the node to find. The value may be a ``dns.name.Name`` or a + ``str``. If absolute, the name must be a subdomain of the zone's origin. If + ``zone.relativize`` is ``True``, then the name will be relativized. + + *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired. + + *covers*, a ``dns.rdatatype.RdataType`` or ``str`` or ``None``, the covered + type. Usually this value is ``dns.rdatatype.NONE``, but if the rdtype is + ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, then the covers value will be + the rdata type the SIG/RRSIG covers. The library treats the SIG and RRSIG types + as if they were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). This + makes RRSIGs much easier to work with than if RRSIGs covering different rdata + types were aggregated into a single RRSIG rdataset. + """ + + name = self._validate_name(name) + rdtype = dns.rdatatype.RdataType.make(rdtype) + covers = dns.rdatatype.RdataType.make(covers) + node = self.get_node(name) + if node is not None: + node.delete_rdataset(self.rdclass, rdtype, covers) + if len(node) == 0: + self.delete_node(name) + + def replace_rdataset( + self, name: dns.name.Name | str, replacement: dns.rdataset.Rdataset + ) -> None: + """Replace an rdataset at name. + + It is not an error if there is no rdataset matching I{replacement}. + + Ownership of the *replacement* object is transferred to the zone; + in other words, this method does not store a copy of *replacement* + at the node, it stores *replacement* itself. + + If the node does not exist, it is created. + + *name*: the name of the node to find. + The value may be a ``dns.name.Name`` or a ``str``. If absolute, the + name must be a subdomain of the zone's origin. If ``zone.relativize`` + is ``True``, then the name will be relativized. + + *replacement*, a ``dns.rdataset.Rdataset``, the replacement rdataset. + """ + + if replacement.rdclass != self.rdclass: + raise ValueError("replacement.rdclass != zone.rdclass") + node = self.find_node(name, True) + node.replace_rdataset(replacement) + + def find_rrset( + self, + name: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + ) -> dns.rrset.RRset: + """Look for an rdataset with the specified name and type in the zone, + and return an RRset encapsulating it. + + This method is less efficient than the similar + ``find_rdataset()`` because it creates an RRset instead of + returning the matching rdataset. It may be more convenient + for some uses since it returns an object which binds the owner + name to the rdataset. + + This method may not be used to create new nodes or rdatasets; + use ``find_rdataset`` instead. + + *name*: the name of the node to find. + The value may be a ``dns.name.Name`` or a ``str``. If absolute, the + name must be a subdomain of the zone's origin. If ``zone.relativize`` + is ``True``, then the name will be relativized. + + *rdtype*, a ``dns.rdatatype.RdataType`` or ``str``, the rdata type desired. + + *covers*, a ``dns.rdatatype.RdataType`` or ``str``, the covered type. + Usually this value is ``dns.rdatatype.NONE``, but if the + rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, + then the covers value will be the rdata type the SIG/RRSIG + covers. The library treats the SIG and RRSIG types as if they + were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). + This makes RRSIGs much easier to work with than if RRSIGs + covering different rdata types were aggregated into a single + RRSIG rdataset. + + *create*, a ``bool``. If true, the node will be created if it does + not exist. + + Raises ``KeyError`` if the name is not known and create was + not specified, or if the name was not a subdomain of the origin. + + Returns a ``dns.rrset.RRset`` or ``None``. + """ + + vname = self._validate_name(name) + rdtype = dns.rdatatype.RdataType.make(rdtype) + covers = dns.rdatatype.RdataType.make(covers) + rdataset = self.nodes[vname].find_rdataset(self.rdclass, rdtype, covers) + rrset = dns.rrset.RRset(vname, self.rdclass, rdtype, covers) + rrset.update(rdataset) + return rrset + + def get_rrset( + self, + name: dns.name.Name | str, + rdtype: dns.rdatatype.RdataType | str, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + ) -> dns.rrset.RRset | None: + """Look for an rdataset with the specified name and type in the zone, + and return an RRset encapsulating it. + + This method is less efficient than the similar ``get_rdataset()`` + because it creates an RRset instead of returning the matching + rdataset. It may be more convenient for some uses since it + returns an object which binds the owner name to the rdataset. + + This method may not be used to create new nodes or rdatasets; + use ``get_rdataset()`` instead. + + *name*: the name of the node to find. + The value may be a ``dns.name.Name`` or a ``str``. If absolute, the + name must be a subdomain of the zone's origin. If ``zone.relativize`` + is ``True``, then the name will be relativized. + + *rdtype*, a ``dns.rdataset.Rdataset`` or ``str``, the rdata type desired. + + *covers*, a ``dns.rdataset.Rdataset`` or ``str``, the covered type. + Usually this value is ``dns.rdatatype.NONE``, but if the + rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, + then the covers value will be the rdata type the SIG/RRSIG + covers. The library treats the SIG and RRSIG types as if they + were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). + This makes RRSIGs much easier to work with than if RRSIGs + covering different rdata types were aggregated into a single + RRSIG rdataset. + + *create*, a ``bool``. If true, the node will be created if it does + not exist. + + Returns a ``dns.rrset.RRset`` or ``None``. + """ + + try: + rrset = self.find_rrset(name, rdtype, covers) + except KeyError: + rrset = None + return rrset + + def iterate_rdatasets( + self, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.ANY, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + ) -> Iterator[Tuple[dns.name.Name, dns.rdataset.Rdataset]]: + """Return a generator which yields (name, rdataset) tuples for + all rdatasets in the zone which have the specified *rdtype* + and *covers*. If *rdtype* is ``dns.rdatatype.ANY``, the default, + then all rdatasets will be matched. + + *rdtype*, a ``dns.rdataset.Rdataset`` or ``str``, the rdata type desired. + + *covers*, a ``dns.rdataset.Rdataset`` or ``str``, the covered type. + Usually this value is ``dns.rdatatype.NONE``, but if the + rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, + then the covers value will be the rdata type the SIG/RRSIG + covers. The library treats the SIG and RRSIG types as if they + were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). + This makes RRSIGs much easier to work with than if RRSIGs + covering different rdata types were aggregated into a single + RRSIG rdataset. + """ + + rdtype = dns.rdatatype.RdataType.make(rdtype) + covers = dns.rdatatype.RdataType.make(covers) + for name, node in self.items(): + for rds in node: + if rdtype == dns.rdatatype.ANY or ( + rds.rdtype == rdtype and rds.covers == covers + ): + yield (name, rds) + + def iterate_rdatas( + self, + rdtype: dns.rdatatype.RdataType | str = dns.rdatatype.ANY, + covers: dns.rdatatype.RdataType | str = dns.rdatatype.NONE, + ) -> Iterator[Tuple[dns.name.Name, int, dns.rdata.Rdata]]: + """Return a generator which yields (name, ttl, rdata) tuples for + all rdatas in the zone which have the specified *rdtype* + and *covers*. If *rdtype* is ``dns.rdatatype.ANY``, the default, + then all rdatas will be matched. + + *rdtype*, a ``dns.rdataset.Rdataset`` or ``str``, the rdata type desired. + + *covers*, a ``dns.rdataset.Rdataset`` or ``str``, the covered type. + Usually this value is ``dns.rdatatype.NONE``, but if the + rdtype is ``dns.rdatatype.SIG`` or ``dns.rdatatype.RRSIG``, + then the covers value will be the rdata type the SIG/RRSIG + covers. The library treats the SIG and RRSIG types as if they + were a family of types, e.g. RRSIG(A), RRSIG(NS), RRSIG(SOA). + This makes RRSIGs much easier to work with than if RRSIGs + covering different rdata types were aggregated into a single + RRSIG rdataset. + """ + + rdtype = dns.rdatatype.RdataType.make(rdtype) + covers = dns.rdatatype.RdataType.make(covers) + for name, node in self.items(): + for rds in node: + if rdtype == dns.rdatatype.ANY or ( + rds.rdtype == rdtype and rds.covers == covers + ): + for rdata in rds: + yield (name, rds.ttl, rdata) + + def to_file( + self, + f: Any, + sorted: bool = True, + relativize: bool = True, + nl: str | None = None, + want_comments: bool = False, + want_origin: bool = False, + ) -> None: + """Write a zone to a file. + + *f*, a file or `str`. If *f* is a string, it is treated + as the name of a file to open. + + *sorted*, a ``bool``. If True, the default, then the file + will be written with the names sorted in DNSSEC order from + least to greatest. Otherwise the names will be written in + whatever order they happen to have in the zone's dictionary. + + *relativize*, a ``bool``. If True, the default, then domain + names in the output will be relativized to the zone's origin + if possible. + + *nl*, a ``str`` or None. The end of line string. If not + ``None``, the output will use the platform's native + end-of-line marker (i.e. LF on POSIX, CRLF on Windows). + + *want_comments*, a ``bool``. If ``True``, emit end-of-line comments + as part of writing the file. If ``False``, the default, do not + emit them. + + *want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at + the start of the file. If ``False``, the default, do not emit + one. + """ + + if isinstance(f, str): + cm: contextlib.AbstractContextManager = open(f, "wb") + else: + cm = contextlib.nullcontext(f) + with cm as f: + # must be in this way, f.encoding may contain None, or even + # attribute may not be there + file_enc = getattr(f, "encoding", None) + if file_enc is None: + file_enc = "utf-8" + + if nl is None: + # binary mode, '\n' is not enough + nl_b = os.linesep.encode(file_enc) + nl = "\n" + elif isinstance(nl, str): + nl_b = nl.encode(file_enc) + else: + nl_b = nl + nl = nl.decode() + + if want_origin: + assert self.origin is not None + l = "$ORIGIN " + self.origin.to_text() + l_b = l.encode(file_enc) + try: + f.write(l_b) + f.write(nl_b) + except TypeError: # textual mode + f.write(l) + f.write(nl) + + if sorted: + names = list(self.keys()) + names.sort() + else: + names = self.keys() + for n in names: + l = self[n].to_text( + n, + origin=self.origin, # pyright: ignore + relativize=relativize, # pyright: ignore + want_comments=want_comments, # pyright: ignore + ) + l_b = l.encode(file_enc) + + try: + f.write(l_b) + f.write(nl_b) + except TypeError: # textual mode + f.write(l) + f.write(nl) + + def to_text( + self, + sorted: bool = True, + relativize: bool = True, + nl: str | None = None, + want_comments: bool = False, + want_origin: bool = False, + ) -> str: + """Return a zone's text as though it were written to a file. + + *sorted*, a ``bool``. If True, the default, then the file + will be written with the names sorted in DNSSEC order from + least to greatest. Otherwise the names will be written in + whatever order they happen to have in the zone's dictionary. + + *relativize*, a ``bool``. If True, the default, then domain + names in the output will be relativized to the zone's origin + if possible. + + *nl*, a ``str`` or None. The end of line string. If not + ``None``, the output will use the platform's native + end-of-line marker (i.e. LF on POSIX, CRLF on Windows). + + *want_comments*, a ``bool``. If ``True``, emit end-of-line comments + as part of writing the file. If ``False``, the default, do not + emit them. + + *want_origin*, a ``bool``. If ``True``, emit a $ORIGIN line at + the start of the output. If ``False``, the default, do not emit + one. + + Returns a ``str``. + """ + temp_buffer = io.StringIO() + self.to_file(temp_buffer, sorted, relativize, nl, want_comments, want_origin) + return_value = temp_buffer.getvalue() + temp_buffer.close() + return return_value + + def check_origin(self) -> None: + """Do some simple checking of the zone's origin. + + Raises ``dns.zone.NoSOA`` if there is no SOA RRset. + + Raises ``dns.zone.NoNS`` if there is no NS RRset. + + Raises ``KeyError`` if there is no origin node. + """ + if self.relativize: + name = dns.name.empty + else: + assert self.origin is not None + name = self.origin + if self.get_rdataset(name, dns.rdatatype.SOA) is None: + raise NoSOA + if self.get_rdataset(name, dns.rdatatype.NS) is None: + raise NoNS + + def get_soa( + self, txn: dns.transaction.Transaction | None = None + ) -> dns.rdtypes.ANY.SOA.SOA: + """Get the zone SOA rdata. + + Raises ``dns.zone.NoSOA`` if there is no SOA RRset. + + Returns a ``dns.rdtypes.ANY.SOA.SOA`` Rdata. + """ + if self.relativize: + origin_name = dns.name.empty + else: + if self.origin is None: + # get_soa() has been called very early, and there must not be + # an SOA if there is no origin. + raise NoSOA + origin_name = self.origin + soa_rds: dns.rdataset.Rdataset | None + if txn: + soa_rds = txn.get(origin_name, dns.rdatatype.SOA) + else: + soa_rds = self.get_rdataset(origin_name, dns.rdatatype.SOA) + if soa_rds is None: + raise NoSOA + else: + soa = cast(dns.rdtypes.ANY.SOA.SOA, soa_rds[0]) + return soa + + def _compute_digest( + self, + hash_algorithm: DigestHashAlgorithm, + scheme: DigestScheme = DigestScheme.SIMPLE, + ) -> bytes: + hashinfo = _digest_hashers.get(hash_algorithm) + if not hashinfo: + raise UnsupportedDigestHashAlgorithm + if scheme != DigestScheme.SIMPLE: + raise UnsupportedDigestScheme + + if self.relativize: + origin_name = dns.name.empty + else: + assert self.origin is not None + origin_name = self.origin + hasher = hashinfo() + for name, node in sorted(self.items()): + rrnamebuf = name.to_digestable(self.origin) + for rdataset in sorted(node, key=lambda rds: (rds.rdtype, rds.covers)): + if name == origin_name and dns.rdatatype.ZONEMD in ( + rdataset.rdtype, + rdataset.covers, + ): + continue + rrfixed = struct.pack( + "!HHI", rdataset.rdtype, rdataset.rdclass, rdataset.ttl + ) + rdatas = [rdata.to_digestable(self.origin) for rdata in rdataset] + for rdata in sorted(rdatas): + rrlen = struct.pack("!H", len(rdata)) + hasher.update(rrnamebuf + rrfixed + rrlen + rdata) + return hasher.digest() + + def compute_digest( + self, + hash_algorithm: DigestHashAlgorithm, + scheme: DigestScheme = DigestScheme.SIMPLE, + ) -> dns.rdtypes.ANY.ZONEMD.ZONEMD: + serial = self.get_soa().serial + digest = self._compute_digest(hash_algorithm, scheme) + return dns.rdtypes.ANY.ZONEMD.ZONEMD( + self.rdclass, dns.rdatatype.ZONEMD, serial, scheme, hash_algorithm, digest + ) + + def verify_digest( + self, zonemd: dns.rdtypes.ANY.ZONEMD.ZONEMD | None = None + ) -> None: + digests: dns.rdataset.Rdataset | List[dns.rdtypes.ANY.ZONEMD.ZONEMD] + if zonemd: + digests = [zonemd] + else: + assert self.origin is not None + rds = self.get_rdataset(self.origin, dns.rdatatype.ZONEMD) + if rds is None: + raise NoDigest + digests = rds + for digest in digests: + try: + computed = self._compute_digest(digest.hash_algorithm, digest.scheme) + if computed == digest.digest: + return + except Exception: + pass + raise DigestVerificationFailure + + # TransactionManager methods + + def reader(self) -> "Transaction": + return Transaction(self, False, Version(self, 1, self.nodes, self.origin)) + + def writer(self, replacement: bool = False) -> "Transaction": + txn = Transaction(self, replacement) + txn._setup_version() + return txn + + def origin_information( + self, + ) -> Tuple[dns.name.Name | None, bool, dns.name.Name | None]: + effective: dns.name.Name | None + if self.relativize: + effective = dns.name.empty + else: + effective = self.origin + return (self.origin, self.relativize, effective) + + def get_class(self): + return self.rdclass + + # Transaction methods + + def _end_read(self, txn): + pass + + def _end_write(self, txn): + pass + + def _commit_version(self, txn, version, origin): + self.nodes = version.nodes + if self.origin is None: + self.origin = origin + + def _get_next_version_id(self) -> int: + # Versions are ephemeral and all have id 1 + return 1 + + +# These classes used to be in dns.versioned, but have moved here so we can use +# the copy-on-write transaction mechanism for both kinds of zones. In a +# regular zone, the version only exists during the transaction, and the nodes +# are regular dns.node.Nodes. + +# A node with a version id. + + +class VersionedNode(dns.node.Node): # lgtm[py/missing-equals] + __slots__ = ["id"] + + def __init__(self): + super().__init__() + # A proper id will get set by the Version + self.id = 0 + + +@dns.immutable.immutable +class ImmutableVersionedNode(VersionedNode): + def __init__(self, node): + super().__init__() + self.id = node.id + self.rdatasets = tuple( + [dns.rdataset.ImmutableRdataset(rds) for rds in node.rdatasets] + ) + + def find_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset: + if create: + raise TypeError("immutable") + return super().find_rdataset(rdclass, rdtype, covers, False) + + def get_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + create: bool = False, + ) -> dns.rdataset.Rdataset | None: + if create: + raise TypeError("immutable") + return super().get_rdataset(rdclass, rdtype, covers, False) + + def delete_rdataset( + self, + rdclass: dns.rdataclass.RdataClass, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType = dns.rdatatype.NONE, + ) -> None: + raise TypeError("immutable") + + def replace_rdataset(self, replacement: dns.rdataset.Rdataset) -> None: + raise TypeError("immutable") + + def is_immutable(self) -> bool: + return True + + +class Version: + def __init__( + self, + zone: Zone, + id: int, + nodes: MutableMapping[dns.name.Name, dns.node.Node] | None = None, + origin: dns.name.Name | None = None, + ): + self.zone = zone + self.id = id + if nodes is not None: + self.nodes = nodes + else: + self.nodes = zone.map_factory() + self.origin = origin + + def _validate_name(self, name: dns.name.Name) -> dns.name.Name: + return _validate_name(name, self.origin, self.zone.relativize) + + def get_node(self, name: dns.name.Name) -> dns.node.Node | None: + name = self._validate_name(name) + return self.nodes.get(name) + + def get_rdataset( + self, + name: dns.name.Name, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType, + ) -> dns.rdataset.Rdataset | None: + node = self.get_node(name) + if node is None: + return None + return node.get_rdataset(self.zone.rdclass, rdtype, covers) + + def keys(self): + return self.nodes.keys() + + def items(self): + return self.nodes.items() + + +class WritableVersion(Version): + def __init__(self, zone: Zone, replacement: bool = False): + # The zone._versions_lock must be held by our caller in a versioned + # zone. + id = zone._get_next_version_id() + super().__init__(zone, id) + if not replacement: + # We copy the map, because that gives us a simple and thread-safe + # way of doing versions, and we have a garbage collector to help + # us. We only make new node objects if we actually change the + # node. + self.nodes.update(zone.nodes) + # We have to copy the zone origin as it may be None in the first + # version, and we don't want to mutate the zone until we commit. + self.origin = zone.origin + self.changed: Set[dns.name.Name] = set() + + def _maybe_cow_with_name( + self, name: dns.name.Name + ) -> Tuple[dns.node.Node, dns.name.Name]: + name = self._validate_name(name) + node = self.nodes.get(name) + if node is None or name not in self.changed: + new_node = self.zone.node_factory() + if hasattr(new_node, "id"): + # We keep doing this for backwards compatibility, as earlier + # code used new_node.id != self.id for the "do we need to CoW?" + # test. Now we use the changed set as this works with both + # regular zones and versioned zones. + # + # We ignore the mypy error as this is safe but it doesn't see it. + new_node.id = self.id # type: ignore + if node is not None: + # moo! copy on write! + new_node.rdatasets.extend(node.rdatasets) + self.nodes[name] = new_node + self.changed.add(name) + return (new_node, name) + else: + return (node, name) + + def _maybe_cow(self, name: dns.name.Name) -> dns.node.Node: + return self._maybe_cow_with_name(name)[0] + + def delete_node(self, name: dns.name.Name) -> None: + name = self._validate_name(name) + if name in self.nodes: + del self.nodes[name] + self.changed.add(name) + + def put_rdataset( + self, name: dns.name.Name, rdataset: dns.rdataset.Rdataset + ) -> None: + node = self._maybe_cow(name) + node.replace_rdataset(rdataset) + + def delete_rdataset( + self, + name: dns.name.Name, + rdtype: dns.rdatatype.RdataType, + covers: dns.rdatatype.RdataType, + ) -> None: + node = self._maybe_cow(name) + node.delete_rdataset(self.zone.rdclass, rdtype, covers) + if len(node) == 0: + del self.nodes[name] + + +@dns.immutable.immutable +class ImmutableVersion(Version): + def __init__(self, version: Version): + if not isinstance(version, WritableVersion): + raise ValueError( + "a dns.zone.ImmutableVersion requires a dns.zone.WritableVersion" + ) + # We tell super() that it's a replacement as we don't want it + # to copy the nodes, as we're about to do that with an + # immutable Dict. + super().__init__(version.zone, True) + # set the right id! + self.id = version.id + # keep the origin + self.origin = version.origin + # Make changed nodes immutable + for name in version.changed: + node = version.nodes.get(name) + # it might not exist if we deleted it in the version + if node: + version.nodes[name] = ImmutableVersionedNode(node) + # We're changing the type of the nodes dictionary here on purpose, so + # we ignore the mypy error. + self.nodes = dns.immutable.Dict( + version.nodes, True, self.zone.map_factory + ) # type: ignore + + +class Transaction(dns.transaction.Transaction): + def __init__(self, zone, replacement, version=None, make_immutable=False): + read_only = version is not None + super().__init__(zone, replacement, read_only) + self.version = version + self.make_immutable = make_immutable + + @property + def zone(self): + return self.manager + + def _setup_version(self): + assert self.version is None + factory = self.manager.writable_version_factory # pyright: ignore + if factory is None: + factory = WritableVersion + self.version = factory(self.zone, self.replacement) # pyright: ignore + + def _get_rdataset(self, name, rdtype, covers): + assert self.version is not None + return self.version.get_rdataset(name, rdtype, covers) + + def _put_rdataset(self, name, rdataset): + assert not self.read_only + assert self.version is not None + self.version.put_rdataset(name, rdataset) + + def _delete_name(self, name): + assert not self.read_only + assert self.version is not None + self.version.delete_node(name) + + def _delete_rdataset(self, name, rdtype, covers): + assert not self.read_only + assert self.version is not None + self.version.delete_rdataset(name, rdtype, covers) + + def _name_exists(self, name): + assert self.version is not None + return self.version.get_node(name) is not None + + def _changed(self): + if self.read_only: + return False + else: + assert self.version is not None + return len(self.version.changed) > 0 + + def _end_transaction(self, commit): + assert self.zone is not None + assert self.version is not None + if self.read_only: + self.zone._end_read(self) # pyright: ignore + elif commit and len(self.version.changed) > 0: + if self.make_immutable: + factory = self.manager.immutable_version_factory # pyright: ignore + if factory is None: + factory = ImmutableVersion + version = factory(self.version) + else: + version = self.version + self.zone._commit_version( # pyright: ignore + self, version, self.version.origin + ) + + else: + # rollback + self.zone._end_write(self) # pyright: ignore + + def _set_origin(self, origin): + assert self.version is not None + if self.version.origin is None: + self.version.origin = origin + + def _iterate_rdatasets(self): + assert self.version is not None + for name, node in self.version.items(): + for rdataset in node: + yield (name, rdataset) + + def _iterate_names(self): + assert self.version is not None + return self.version.keys() + + def _get_node(self, name): + assert self.version is not None + return self.version.get_node(name) + + def _origin_information(self): + assert self.version is not None + (absolute, relativize, effective) = self.manager.origin_information() + if absolute is None and self.version.origin is not None: + # No origin has been committed yet, but we've learned one as part of + # this txn. Use it. + absolute = self.version.origin + if relativize: + effective = dns.name.empty + else: + effective = absolute + return (absolute, relativize, effective) + + +def _from_text( + text: Any, + origin: dns.name.Name | str | None = None, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + relativize: bool = True, + zone_factory: Any = Zone, + filename: str | None = None, + allow_include: bool = False, + check_origin: bool = True, + idna_codec: dns.name.IDNACodec | None = None, + allow_directives: bool | Iterable[str] = True, +) -> Zone: + # See the comments for the public APIs from_text() and from_file() for + # details. + + # 'text' can also be a file, but we don't publish that fact + # since it's an implementation detail. The official file + # interface is from_file(). + + if filename is None: + filename = "" + zone = zone_factory(origin, rdclass, relativize=relativize) + with zone.writer(True) as txn: + tok = dns.tokenizer.Tokenizer(text, filename, idna_codec=idna_codec) + reader = dns.zonefile.Reader( + tok, + rdclass, + txn, + allow_include=allow_include, + allow_directives=allow_directives, + ) + try: + reader.read() + except dns.zonefile.UnknownOrigin: + # for backwards compatibility + raise UnknownOrigin + # Now that we're done reading, do some basic checking of the zone. + if check_origin: + zone.check_origin() + return zone + + +def from_text( + text: str, + origin: dns.name.Name | str | None = None, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + relativize: bool = True, + zone_factory: Any = Zone, + filename: str | None = None, + allow_include: bool = False, + check_origin: bool = True, + idna_codec: dns.name.IDNACodec | None = None, + allow_directives: bool | Iterable[str] = True, +) -> Zone: + """Build a zone object from a zone file format string. + + *text*, a ``str``, the zone file format input. + + *origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin + of the zone; if not specified, the first ``$ORIGIN`` statement in the + zone file will determine the origin of the zone. + + *rdclass*, a ``dns.rdataclass.RdataClass``, the zone's rdata class; the default is + class IN. + + *relativize*, a ``bool``, determine's whether domain names are + relativized to the zone's origin. The default is ``True``. + + *zone_factory*, the zone factory to use or ``None``. If ``None``, then + ``dns.zone.Zone`` will be used. The value may be any class or callable + that returns a subclass of ``dns.zone.Zone``. + + *filename*, a ``str`` or ``None``, the filename to emit when + describing where an error occurred; the default is ``''``. + + *allow_include*, a ``bool``. If ``True``, the default, then ``$INCLUDE`` + directives are permitted. If ``False``, then encoutering a ``$INCLUDE`` + will raise a ``SyntaxError`` exception. + + *check_origin*, a ``bool``. If ``True``, the default, then sanity + checks of the origin node will be made by calling the zone's + ``check_origin()`` method. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + *allow_directives*, a ``bool`` or an iterable of `str`. If ``True``, the default, + then directives are permitted, and the *allow_include* parameter controls whether + ``$INCLUDE`` is permitted. If ``False`` or an empty iterable, then no directive + processing is done and any directive-like text will be treated as a regular owner + name. If a non-empty iterable, then only the listed directives (including the + ``$``) are allowed. + + Raises ``dns.zone.NoSOA`` if there is no SOA RRset. + + Raises ``dns.zone.NoNS`` if there is no NS RRset. + + Raises ``KeyError`` if there is no origin node. + + Returns a subclass of ``dns.zone.Zone``. + """ + return _from_text( + text, + origin, + rdclass, + relativize, + zone_factory, + filename, + allow_include, + check_origin, + idna_codec, + allow_directives, + ) + + +def from_file( + f: Any, + origin: dns.name.Name | str | None = None, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + relativize: bool = True, + zone_factory: Any = Zone, + filename: str | None = None, + allow_include: bool = True, + check_origin: bool = True, + idna_codec: dns.name.IDNACodec | None = None, + allow_directives: bool | Iterable[str] = True, +) -> Zone: + """Read a zone file and build a zone object. + + *f*, a file or ``str``. If *f* is a string, it is treated + as the name of a file to open. + + *origin*, a ``dns.name.Name``, a ``str``, or ``None``. The origin + of the zone; if not specified, the first ``$ORIGIN`` statement in the + zone file will determine the origin of the zone. + + *rdclass*, an ``int``, the zone's rdata class; the default is class IN. + + *relativize*, a ``bool``, determine's whether domain names are + relativized to the zone's origin. The default is ``True``. + + *zone_factory*, the zone factory to use or ``None``. If ``None``, then + ``dns.zone.Zone`` will be used. The value may be any class or callable + that returns a subclass of ``dns.zone.Zone``. + + *filename*, a ``str`` or ``None``, the filename to emit when + describing where an error occurred; the default is ``''``. + + *allow_include*, a ``bool``. If ``True``, the default, then ``$INCLUDE`` + directives are permitted. If ``False``, then encoutering a ``$INCLUDE`` + will raise a ``SyntaxError`` exception. + + *check_origin*, a ``bool``. If ``True``, the default, then sanity + checks of the origin node will be made by calling the zone's + ``check_origin()`` method. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. + + *allow_directives*, a ``bool`` or an iterable of `str`. If ``True``, the default, + then directives are permitted, and the *allow_include* parameter controls whether + ``$INCLUDE`` is permitted. If ``False`` or an empty iterable, then no directive + processing is done and any directive-like text will be treated as a regular owner + name. If a non-empty iterable, then only the listed directives (including the + ``$``) are allowed. + + Raises ``dns.zone.NoSOA`` if there is no SOA RRset. + + Raises ``dns.zone.NoNS`` if there is no NS RRset. + + Raises ``KeyError`` if there is no origin node. + + Returns a subclass of ``dns.zone.Zone``. + """ + + if isinstance(f, str): + if filename is None: + filename = f + cm: contextlib.AbstractContextManager = open(f, encoding="utf-8") + else: + cm = contextlib.nullcontext(f) + with cm as f: + return _from_text( + f, + origin, + rdclass, + relativize, + zone_factory, + filename, + allow_include, + check_origin, + idna_codec, + allow_directives, + ) + assert False # make mypy happy lgtm[py/unreachable-statement] + + +def from_xfr( + xfr: Any, + zone_factory: Any = Zone, + relativize: bool = True, + check_origin: bool = True, +) -> Zone: + """Convert the output of a zone transfer generator into a zone object. + + *xfr*, a generator of ``dns.message.Message`` objects, typically + ``dns.query.xfr()``. + + *relativize*, a ``bool``, determine's whether domain names are + relativized to the zone's origin. The default is ``True``. + It is essential that the relativize setting matches the one specified + to the generator. + + *check_origin*, a ``bool``. If ``True``, the default, then sanity + checks of the origin node will be made by calling the zone's + ``check_origin()`` method. + + Raises ``dns.zone.NoSOA`` if there is no SOA RRset. + + Raises ``dns.zone.NoNS`` if there is no NS RRset. + + Raises ``KeyError`` if there is no origin node. + + Raises ``ValueError`` if no messages are yielded by the generator. + + Returns a subclass of ``dns.zone.Zone``. + """ + + z = None + for r in xfr: + if z is None: + if relativize: + origin = r.origin + else: + origin = r.answer[0].name + rdclass = r.answer[0].rdclass + z = zone_factory(origin, rdclass, relativize=relativize) + for rrset in r.answer: + znode = z.nodes.get(rrset.name) + if not znode: + znode = z.node_factory() + z.nodes[rrset.name] = znode + zrds = znode.find_rdataset(rrset.rdclass, rrset.rdtype, rrset.covers, True) + zrds.update_ttl(rrset.ttl) + for rd in rrset: + zrds.add(rd) + if z is None: + raise ValueError("empty transfer") + if check_origin: + z.check_origin() + return z diff --git a/netdeploy/lib/python3.11/site-packages/dns/zonefile.py b/netdeploy/lib/python3.11/site-packages/dns/zonefile.py new file mode 100644 index 0000000..7a81454 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/zonefile.py @@ -0,0 +1,756 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +# Copyright (C) 2003-2007, 2009-2011 Nominum, Inc. +# +# Permission to use, copy, modify, and distribute this software and its +# documentation for any purpose with or without fee is hereby granted, +# provided that the above copyright notice and this permission notice +# appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +# OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +"""DNS Zones.""" + +import re +import sys +from typing import Any, Iterable, List, Set, Tuple, cast + +import dns.exception +import dns.grange +import dns.name +import dns.node +import dns.rdata +import dns.rdataclass +import dns.rdatatype +import dns.rdtypes.ANY.SOA +import dns.rrset +import dns.tokenizer +import dns.transaction +import dns.ttl + + +class UnknownOrigin(dns.exception.DNSException): + """Unknown origin""" + + +class CNAMEAndOtherData(dns.exception.DNSException): + """A node has a CNAME and other data""" + + +def _check_cname_and_other_data(txn, name, rdataset): + rdataset_kind = dns.node.NodeKind.classify_rdataset(rdataset) + node = txn.get_node(name) + if node is None: + # empty nodes are neutral. + return + node_kind = node.classify() + if ( + node_kind == dns.node.NodeKind.CNAME + and rdataset_kind == dns.node.NodeKind.REGULAR + ): + raise CNAMEAndOtherData("rdataset type is not compatible with a CNAME node") + elif ( + node_kind == dns.node.NodeKind.REGULAR + and rdataset_kind == dns.node.NodeKind.CNAME + ): + raise CNAMEAndOtherData( + "CNAME rdataset is not compatible with a regular data node" + ) + # Otherwise at least one of the node and the rdataset is neutral, so + # adding the rdataset is ok + + +SavedStateType = Tuple[ + dns.tokenizer.Tokenizer, + dns.name.Name | None, # current_origin + dns.name.Name | None, # last_name + Any | None, # current_file + int, # last_ttl + bool, # last_ttl_known + int, # default_ttl + bool, +] # default_ttl_known + + +def _upper_dollarize(s): + s = s.upper() + if not s.startswith("$"): + s = "$" + s + return s + + +class Reader: + """Read a DNS zone file into a transaction.""" + + def __init__( + self, + tok: dns.tokenizer.Tokenizer, + rdclass: dns.rdataclass.RdataClass, + txn: dns.transaction.Transaction, + allow_include: bool = False, + allow_directives: bool | Iterable[str] = True, + force_name: dns.name.Name | None = None, + force_ttl: int | None = None, + force_rdclass: dns.rdataclass.RdataClass | None = None, + force_rdtype: dns.rdatatype.RdataType | None = None, + default_ttl: int | None = None, + ): + self.tok = tok + (self.zone_origin, self.relativize, _) = txn.manager.origin_information() + self.current_origin = self.zone_origin + self.last_ttl = 0 + self.last_ttl_known = False + if force_ttl is not None: + default_ttl = force_ttl + if default_ttl is None: + self.default_ttl = 0 + self.default_ttl_known = False + else: + self.default_ttl = default_ttl + self.default_ttl_known = True + self.last_name = self.current_origin + self.zone_rdclass = rdclass + self.txn = txn + self.saved_state: List[SavedStateType] = [] + self.current_file: Any | None = None + self.allowed_directives: Set[str] + if allow_directives is True: + self.allowed_directives = {"$GENERATE", "$ORIGIN", "$TTL"} + if allow_include: + self.allowed_directives.add("$INCLUDE") + elif allow_directives is False: + # allow_include was ignored in earlier releases if allow_directives was + # False, so we continue that. + self.allowed_directives = set() + else: + # Note that if directives are explicitly specified, then allow_include + # is ignored. + self.allowed_directives = set(_upper_dollarize(d) for d in allow_directives) + self.force_name = force_name + self.force_ttl = force_ttl + self.force_rdclass = force_rdclass + self.force_rdtype = force_rdtype + self.txn.check_put_rdataset(_check_cname_and_other_data) + + def _eat_line(self): + while 1: + token = self.tok.get() + if token.is_eol_or_eof(): + break + + def _get_identifier(self): + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + return token + + def _rr_line(self): + """Process one line from a DNS zone file.""" + token = None + # Name + if self.force_name is not None: + name = self.force_name + else: + if self.current_origin is None: + raise UnknownOrigin + token = self.tok.get(want_leading=True) + if not token.is_whitespace(): + self.last_name = self.tok.as_name(token, self.current_origin) + else: + token = self.tok.get() + if token.is_eol_or_eof(): + # treat leading WS followed by EOL/EOF as if they were EOL/EOF. + return + self.tok.unget(token) + name = self.last_name + if name is None: + raise dns.exception.SyntaxError("the last used name is undefined") + assert self.zone_origin is not None + if not name.is_subdomain(self.zone_origin): + self._eat_line() + return + if self.relativize: + name = name.relativize(self.zone_origin) + + # TTL + if self.force_ttl is not None: + ttl = self.force_ttl + self.last_ttl = ttl + self.last_ttl_known = True + else: + token = self._get_identifier() + ttl = None + try: + ttl = dns.ttl.from_text(token.value) + self.last_ttl = ttl + self.last_ttl_known = True + token = None + except dns.ttl.BadTTL: + self.tok.unget(token) + + # Class + if self.force_rdclass is not None: + rdclass = self.force_rdclass + else: + token = self._get_identifier() + try: + rdclass = dns.rdataclass.from_text(token.value) + except dns.exception.SyntaxError: + raise + except Exception: + rdclass = self.zone_rdclass + self.tok.unget(token) + if rdclass != self.zone_rdclass: + raise dns.exception.SyntaxError("RR class is not zone's class") + + if ttl is None: + # support for syntax + token = self._get_identifier() + ttl = None + try: + ttl = dns.ttl.from_text(token.value) + self.last_ttl = ttl + self.last_ttl_known = True + token = None + except dns.ttl.BadTTL: + if self.default_ttl_known: + ttl = self.default_ttl + elif self.last_ttl_known: + ttl = self.last_ttl + self.tok.unget(token) + + # Type + if self.force_rdtype is not None: + rdtype = self.force_rdtype + else: + token = self._get_identifier() + try: + rdtype = dns.rdatatype.from_text(token.value) + except Exception: + raise dns.exception.SyntaxError(f"unknown rdatatype '{token.value}'") + + try: + rd = dns.rdata.from_text( + rdclass, + rdtype, + self.tok, + self.current_origin, + self.relativize, + self.zone_origin, + ) + except dns.exception.SyntaxError: + # Catch and reraise. + raise + except Exception: + # All exceptions that occur in the processing of rdata + # are treated as syntax errors. This is not strictly + # correct, but it is correct almost all of the time. + # We convert them to syntax errors so that we can emit + # helpful filename:line info. + (ty, va) = sys.exc_info()[:2] + raise dns.exception.SyntaxError(f"caught exception {str(ty)}: {str(va)}") + + if not self.default_ttl_known and rdtype == dns.rdatatype.SOA: + # The pre-RFC2308 and pre-BIND9 behavior inherits the zone default + # TTL from the SOA minttl if no $TTL statement is present before the + # SOA is parsed. + soa_rd = cast(dns.rdtypes.ANY.SOA.SOA, rd) + self.default_ttl = soa_rd.minimum + self.default_ttl_known = True + if ttl is None: + # if we didn't have a TTL on the SOA, set it! + ttl = soa_rd.minimum + + # TTL check. We had to wait until now to do this as the SOA RR's + # own TTL can be inferred from its minimum. + if ttl is None: + raise dns.exception.SyntaxError("Missing default TTL value") + + self.txn.add(name, ttl, rd) + + def _parse_modify(self, side: str) -> Tuple[str, str, int, int, str]: + # Here we catch everything in '{' '}' in a group so we can replace it + # with ''. + is_generate1 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+),(.)}).*$") + is_generate2 = re.compile(r"^.*\$({(\+|-?)(\d+)}).*$") + is_generate3 = re.compile(r"^.*\$({(\+|-?)(\d+),(\d+)}).*$") + # Sometimes there are modifiers in the hostname. These come after + # the dollar sign. They are in the form: ${offset[,width[,base]]}. + # Make names + mod = "" + sign = "+" + offset = "0" + width = "0" + base = "d" + g1 = is_generate1.match(side) + if g1: + mod, sign, offset, width, base = g1.groups() + if sign == "": + sign = "+" + else: + g2 = is_generate2.match(side) + if g2: + mod, sign, offset = g2.groups() + if sign == "": + sign = "+" + width = "0" + base = "d" + else: + g3 = is_generate3.match(side) + if g3: + mod, sign, offset, width = g3.groups() + if sign == "": + sign = "+" + base = "d" + + ioffset = int(offset) + iwidth = int(width) + + if sign not in ["+", "-"]: + raise dns.exception.SyntaxError(f"invalid offset sign {sign}") + if base not in ["d", "o", "x", "X", "n", "N"]: + raise dns.exception.SyntaxError(f"invalid type {base}") + + return mod, sign, ioffset, iwidth, base + + def _generate_line(self): + # range lhs [ttl] [class] type rhs [ comment ] + """Process one line containing the GENERATE statement from a DNS + zone file.""" + if self.current_origin is None: + raise UnknownOrigin + + token = self.tok.get() + # Range (required) + try: + start, stop, step = dns.grange.from_text(token.value) + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + except Exception: + raise dns.exception.SyntaxError + + # lhs (required) + try: + lhs = token.value + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + except Exception: + raise dns.exception.SyntaxError + + # TTL + try: + ttl = dns.ttl.from_text(token.value) + self.last_ttl = ttl + self.last_ttl_known = True + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + except dns.ttl.BadTTL: + if not (self.last_ttl_known or self.default_ttl_known): + raise dns.exception.SyntaxError("Missing default TTL value") + if self.default_ttl_known: + ttl = self.default_ttl + elif self.last_ttl_known: + ttl = self.last_ttl + else: + # We don't go to the extra "look at the SOA" level of effort for + # $GENERATE, because the user really ought to have defined a TTL + # somehow! + raise dns.exception.SyntaxError("Missing default TTL value") + + # Class + try: + rdclass = dns.rdataclass.from_text(token.value) + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + except dns.exception.SyntaxError: + raise dns.exception.SyntaxError + except Exception: + rdclass = self.zone_rdclass + if rdclass != self.zone_rdclass: + raise dns.exception.SyntaxError("RR class is not zone's class") + # Type + try: + rdtype = dns.rdatatype.from_text(token.value) + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError + except Exception: + raise dns.exception.SyntaxError(f"unknown rdatatype '{token.value}'") + + # rhs (required) + rhs = token.value + + def _calculate_index(counter: int, offset_sign: str, offset: int) -> int: + """Calculate the index from the counter and offset.""" + if offset_sign == "-": + offset *= -1 + return counter + offset + + def _format_index(index: int, base: str, width: int) -> str: + """Format the index with the given base, and zero-fill it + to the given width.""" + if base in ["d", "o", "x", "X"]: + return format(index, base).zfill(width) + + # base can only be n or N here + hexa = _format_index(index, "x", width) + nibbles = ".".join(hexa[::-1])[:width] + if base == "N": + nibbles = nibbles.upper() + return nibbles + + lmod, lsign, loffset, lwidth, lbase = self._parse_modify(lhs) + rmod, rsign, roffset, rwidth, rbase = self._parse_modify(rhs) + for i in range(start, stop + 1, step): + # +1 because bind is inclusive and python is exclusive + + lindex = _calculate_index(i, lsign, loffset) + rindex = _calculate_index(i, rsign, roffset) + + lzfindex = _format_index(lindex, lbase, lwidth) + rzfindex = _format_index(rindex, rbase, rwidth) + + name = lhs.replace(f"${lmod}", lzfindex) + rdata = rhs.replace(f"${rmod}", rzfindex) + + self.last_name = dns.name.from_text( + name, self.current_origin, self.tok.idna_codec + ) + name = self.last_name + assert self.zone_origin is not None + if not name.is_subdomain(self.zone_origin): + self._eat_line() + return + if self.relativize: + name = name.relativize(self.zone_origin) + + try: + rd = dns.rdata.from_text( + rdclass, + rdtype, + rdata, + self.current_origin, + self.relativize, + self.zone_origin, + ) + except dns.exception.SyntaxError: + # Catch and reraise. + raise + except Exception: + # All exceptions that occur in the processing of rdata + # are treated as syntax errors. This is not strictly + # correct, but it is correct almost all of the time. + # We convert them to syntax errors so that we can emit + # helpful filename:line info. + (ty, va) = sys.exc_info()[:2] + raise dns.exception.SyntaxError( + f"caught exception {str(ty)}: {str(va)}" + ) + + self.txn.add(name, ttl, rd) + + def read(self) -> None: + """Read a DNS zone file and build a zone object. + + @raises dns.zone.NoSOA: No SOA RR was found at the zone origin + @raises dns.zone.NoNS: No NS RRset was found at the zone origin + """ + + try: + while 1: + token = self.tok.get(True, True) + if token.is_eof(): + if self.current_file is not None: + self.current_file.close() + if len(self.saved_state) > 0: + ( + self.tok, + self.current_origin, + self.last_name, + self.current_file, + self.last_ttl, + self.last_ttl_known, + self.default_ttl, + self.default_ttl_known, + ) = self.saved_state.pop(-1) + continue + break + elif token.is_eol(): + continue + elif token.is_comment(): + self.tok.get_eol() + continue + elif token.value[0] == "$" and len(self.allowed_directives) > 0: + # Note that we only run directive processing code if at least + # one directive is allowed in order to be backwards compatible + c = token.value.upper() + if c not in self.allowed_directives: + raise dns.exception.SyntaxError( + f"zone file directive '{c}' is not allowed" + ) + if c == "$TTL": + token = self.tok.get() + if not token.is_identifier(): + raise dns.exception.SyntaxError("bad $TTL") + self.default_ttl = dns.ttl.from_text(token.value) + self.default_ttl_known = True + self.tok.get_eol() + elif c == "$ORIGIN": + self.current_origin = self.tok.get_name() + self.tok.get_eol() + if self.zone_origin is None: + self.zone_origin = self.current_origin + self.txn._set_origin(self.current_origin) + elif c == "$INCLUDE": + token = self.tok.get() + filename = token.value + token = self.tok.get() + new_origin: dns.name.Name | None + if token.is_identifier(): + new_origin = dns.name.from_text( + token.value, self.current_origin, self.tok.idna_codec + ) + self.tok.get_eol() + elif not token.is_eol_or_eof(): + raise dns.exception.SyntaxError("bad origin in $INCLUDE") + else: + new_origin = self.current_origin + self.saved_state.append( + ( + self.tok, + self.current_origin, + self.last_name, + self.current_file, + self.last_ttl, + self.last_ttl_known, + self.default_ttl, + self.default_ttl_known, + ) + ) + self.current_file = open(filename, encoding="utf-8") + self.tok = dns.tokenizer.Tokenizer(self.current_file, filename) + self.current_origin = new_origin + elif c == "$GENERATE": + self._generate_line() + else: + raise dns.exception.SyntaxError( + f"Unknown zone file directive '{c}'" + ) + continue + self.tok.unget(token) + self._rr_line() + except dns.exception.SyntaxError as detail: + (filename, line_number) = self.tok.where() + if detail is None: + detail = "syntax error" + ex = dns.exception.SyntaxError(f"{filename}:{line_number}: {detail}") + tb = sys.exc_info()[2] + raise ex.with_traceback(tb) from None + + +class RRsetsReaderTransaction(dns.transaction.Transaction): + def __init__(self, manager, replacement, read_only): + assert not read_only + super().__init__(manager, replacement, read_only) + self.rdatasets = {} + + def _get_rdataset(self, name, rdtype, covers): + return self.rdatasets.get((name, rdtype, covers)) + + def _get_node(self, name): + rdatasets = [] + for (rdataset_name, _, _), rdataset in self.rdatasets.items(): + if name == rdataset_name: + rdatasets.append(rdataset) + if len(rdatasets) == 0: + return None + node = dns.node.Node() + node.rdatasets = rdatasets + return node + + def _put_rdataset(self, name, rdataset): + self.rdatasets[(name, rdataset.rdtype, rdataset.covers)] = rdataset + + def _delete_name(self, name): + # First remove any changes involving the name + remove = [] + for key in self.rdatasets: + if key[0] == name: + remove.append(key) + if len(remove) > 0: + for key in remove: + del self.rdatasets[key] + + def _delete_rdataset(self, name, rdtype, covers): + try: + del self.rdatasets[(name, rdtype, covers)] + except KeyError: + pass + + def _name_exists(self, name): + for n, _, _ in self.rdatasets: + if n == name: + return True + return False + + def _changed(self): + return len(self.rdatasets) > 0 + + def _end_transaction(self, commit): + if commit and self._changed(): + rrsets = [] + for (name, _, _), rdataset in self.rdatasets.items(): + rrset = dns.rrset.RRset( + name, rdataset.rdclass, rdataset.rdtype, rdataset.covers + ) + rrset.update(rdataset) + rrsets.append(rrset) + self.manager.set_rrsets(rrsets) # pyright: ignore + + def _set_origin(self, origin): + pass + + def _iterate_rdatasets(self): + raise NotImplementedError # pragma: no cover + + def _iterate_names(self): + raise NotImplementedError # pragma: no cover + + +class RRSetsReaderManager(dns.transaction.TransactionManager): + def __init__( + self, + origin: dns.name.Name | None = dns.name.root, + relativize: bool = False, + rdclass: dns.rdataclass.RdataClass = dns.rdataclass.IN, + ): + self.origin = origin + self.relativize = relativize + self.rdclass = rdclass + self.rrsets: List[dns.rrset.RRset] = [] + + def reader(self): # pragma: no cover + raise NotImplementedError + + def writer(self, replacement=False): + assert replacement is True + return RRsetsReaderTransaction(self, True, False) + + def get_class(self): + return self.rdclass + + def origin_information(self): + if self.relativize: + effective = dns.name.empty + else: + effective = self.origin + return (self.origin, self.relativize, effective) + + def set_rrsets(self, rrsets: List[dns.rrset.RRset]) -> None: + self.rrsets = rrsets + + +def read_rrsets( + text: Any, + name: dns.name.Name | str | None = None, + ttl: int | None = None, + rdclass: dns.rdataclass.RdataClass | str | None = dns.rdataclass.IN, + default_rdclass: dns.rdataclass.RdataClass | str = dns.rdataclass.IN, + rdtype: dns.rdatatype.RdataType | str | None = None, + default_ttl: int | str | None = None, + idna_codec: dns.name.IDNACodec | None = None, + origin: dns.name.Name | str | None = dns.name.root, + relativize: bool = False, +) -> List[dns.rrset.RRset]: + """Read one or more rrsets from the specified text, possibly subject + to restrictions. + + *text*, a file object or a string, is the input to process. + + *name*, a string, ``dns.name.Name``, or ``None``, is the owner name of + the rrset. If not ``None``, then the owner name is "forced", and the + input must not specify an owner name. If ``None``, then any owner names + are allowed and must be present in the input. + + *ttl*, an ``int``, string, or None. If not ``None``, the the TTL is + forced to be the specified value and the input must not specify a TTL. + If ``None``, then a TTL may be specified in the input. If it is not + specified, then the *default_ttl* will be used. + + *rdclass*, a ``dns.rdataclass.RdataClass``, string, or ``None``. If + not ``None``, then the class is forced to the specified value, and the + input must not specify a class. If ``None``, then the input may specify + a class that matches *default_rdclass*. Note that it is not possible to + return rrsets with differing classes; specifying ``None`` for the class + simply allows the user to optionally type a class as that may be convenient + when cutting and pasting. + + *default_rdclass*, a ``dns.rdataclass.RdataClass`` or string. The class + of the returned rrsets. + + *rdtype*, a ``dns.rdatatype.RdataType``, string, or ``None``. If not + ``None``, then the type is forced to the specified value, and the + input must not specify a type. If ``None``, then a type must be present + for each RR. + + *default_ttl*, an ``int``, string, or ``None``. If not ``None``, then if + the TTL is not forced and is not specified, then this value will be used. + if ``None``, then if the TTL is not forced an error will occur if the TTL + is not specified. + + *idna_codec*, a ``dns.name.IDNACodec``, specifies the IDNA + encoder/decoder. If ``None``, the default IDNA 2003 encoder/decoder + is used. Note that codecs only apply to the owner name; dnspython does + not do IDNA for names in rdata, as there is no IDNA zonefile format. + + *origin*, a string, ``dns.name.Name``, or ``None``, is the origin for any + relative names in the input, and also the origin to relativize to if + *relativize* is ``True``. + + *relativize*, a bool. If ``True``, names are relativized to the *origin*; + if ``False`` then any relative names in the input are made absolute by + appending the *origin*. + """ + if isinstance(origin, str): + origin = dns.name.from_text(origin, dns.name.root, idna_codec) + if isinstance(name, str): + name = dns.name.from_text(name, origin, idna_codec) + if isinstance(ttl, str): + ttl = dns.ttl.from_text(ttl) + if isinstance(default_ttl, str): + default_ttl = dns.ttl.from_text(default_ttl) + if rdclass is not None: + rdclass = dns.rdataclass.RdataClass.make(rdclass) + else: + rdclass = None + default_rdclass = dns.rdataclass.RdataClass.make(default_rdclass) + if rdtype is not None: + rdtype = dns.rdatatype.RdataType.make(rdtype) + else: + rdtype = None + manager = RRSetsReaderManager(origin, relativize, default_rdclass) + with manager.writer(True) as txn: + tok = dns.tokenizer.Tokenizer(text, "", idna_codec=idna_codec) + reader = Reader( + tok, + default_rdclass, + txn, + allow_directives=False, + force_name=name, + force_ttl=ttl, + force_rdclass=rdclass, + force_rdtype=rdtype, + default_ttl=default_ttl, + ) + reader.read() + return manager.rrsets diff --git a/netdeploy/lib/python3.11/site-packages/dns/zonetypes.py b/netdeploy/lib/python3.11/site-packages/dns/zonetypes.py new file mode 100644 index 0000000..195ee2e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dns/zonetypes.py @@ -0,0 +1,37 @@ +# Copyright (C) Dnspython Contributors, see LICENSE for text of ISC license + +"""Common zone-related types.""" + +# This is a separate file to avoid import circularity between dns.zone and +# the implementation of the ZONEMD type. + +import hashlib + +import dns.enum + + +class DigestScheme(dns.enum.IntEnum): + """ZONEMD Scheme""" + + SIMPLE = 1 + + @classmethod + def _maximum(cls): + return 255 + + +class DigestHashAlgorithm(dns.enum.IntEnum): + """ZONEMD Hash Algorithm""" + + SHA384 = 1 + SHA512 = 2 + + @classmethod + def _maximum(cls): + return 255 + + +_digest_hashers = { + DigestHashAlgorithm.SHA384: hashlib.sha384, + DigestHashAlgorithm.SHA512: hashlib.sha512, +} diff --git a/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/INSTALLER b/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/METADATA b/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/METADATA new file mode 100644 index 0000000..eaaf09b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/METADATA @@ -0,0 +1,149 @@ +Metadata-Version: 2.4 +Name: dnspython +Version: 2.8.0 +Summary: DNS toolkit +Project-URL: homepage, https://www.dnspython.org +Project-URL: repository, https://github.com/rthalley/dnspython.git +Project-URL: documentation, https://dnspython.readthedocs.io/en/stable/ +Project-URL: issues, https://github.com/rthalley/dnspython/issues +Author-email: Bob Halley +License: ISC +License-File: LICENSE +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Intended Audience :: System Administrators +Classifier: License :: OSI Approved :: ISC License (ISCL) +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Programming Language :: Python :: 3.13 +Classifier: Topic :: Internet :: Name Service (DNS) +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.10 +Provides-Extra: dev +Requires-Dist: black>=25.1.0; extra == 'dev' +Requires-Dist: coverage>=7.0; extra == 'dev' +Requires-Dist: flake8>=7; extra == 'dev' +Requires-Dist: hypercorn>=0.17.0; extra == 'dev' +Requires-Dist: mypy>=1.17; extra == 'dev' +Requires-Dist: pylint>=3; extra == 'dev' +Requires-Dist: pytest-cov>=6.2.0; extra == 'dev' +Requires-Dist: pytest>=8.4; extra == 'dev' +Requires-Dist: quart-trio>=0.12.0; extra == 'dev' +Requires-Dist: sphinx-rtd-theme>=3.0.0; extra == 'dev' +Requires-Dist: sphinx>=8.2.0; extra == 'dev' +Requires-Dist: twine>=6.1.0; extra == 'dev' +Requires-Dist: wheel>=0.45.0; extra == 'dev' +Provides-Extra: dnssec +Requires-Dist: cryptography>=45; extra == 'dnssec' +Provides-Extra: doh +Requires-Dist: h2>=4.2.0; extra == 'doh' +Requires-Dist: httpcore>=1.0.0; extra == 'doh' +Requires-Dist: httpx>=0.28.0; extra == 'doh' +Provides-Extra: doq +Requires-Dist: aioquic>=1.2.0; extra == 'doq' +Provides-Extra: idna +Requires-Dist: idna>=3.10; extra == 'idna' +Provides-Extra: trio +Requires-Dist: trio>=0.30; extra == 'trio' +Provides-Extra: wmi +Requires-Dist: wmi>=1.5.1; (platform_system == 'Windows') and extra == 'wmi' +Description-Content-Type: text/markdown + +# dnspython + +[![Build Status](https://github.com/rthalley/dnspython/actions/workflows/ci.yml/badge.svg)](https://github.com/rthalley/dnspython/actions/) +[![Documentation Status](https://readthedocs.org/projects/dnspython/badge/?version=latest)](https://dnspython.readthedocs.io/en/latest/?badge=latest) +[![PyPI version](https://badge.fury.io/py/dnspython.svg)](https://badge.fury.io/py/dnspython) +[![License: ISC](https://img.shields.io/badge/License-ISC-brightgreen.svg)](https://opensource.org/licenses/ISC) +[![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) + +## INTRODUCTION + +`dnspython` is a DNS toolkit for Python. It supports almost all record types. It +can be used for queries, zone transfers, and dynamic updates. It supports +TSIG-authenticated messages and EDNS0. + +`dnspython` provides both high- and low-level access to DNS. The high-level +classes perform queries for data of a given name, type, and class, and return an +answer set. The low-level classes allow direct manipulation of DNS zones, +messages, names, and records. + +To see a few of the ways `dnspython` can be used, look in the `examples/` +directory. + +`dnspython` is a utility to work with DNS, `/etc/hosts` is thus not used. For +simple forward DNS lookups, it's better to use `socket.getaddrinfo()` or +`socket.gethostbyname()`. + +`dnspython` originated at Nominum where it was developed to facilitate the +testing of DNS software. + +## ABOUT THIS RELEASE + +This is of `dnspython` 2.8.0. +Please read +[What's New](https://dnspython.readthedocs.io/en/stable/whatsnew.html) for +information about the changes in this release. + +## INSTALLATION + +* Many distributions have dnspython packaged for you, so you should check there + first. +* To use a wheel downloaded from PyPi, run: + +``` + pip install dnspython +``` + +* To install from the source code, go into the top-level of the source code + and run: + +``` + pip install --upgrade pip build + python -m build + pip install dist/*.whl +``` + +* To install the latest from the main branch, run +`pip install git+https://github.com/rthalley/dnspython.git` + +`dnspython`'s default installation does not depend on any modules other than +those in the Python standard library. To use some features, additional modules +must be installed. For convenience, `pip` options are defined for the +requirements. + +If you want to use DNS-over-HTTPS, run +`pip install dnspython[doh]`. + +If you want to use DNSSEC functionality, run +`pip install dnspython[dnssec]`. + +If you want to use internationalized domain names (IDNA) +functionality, you must run +`pip install dnspython[idna]` + +If you want to use the Trio asynchronous I/O package, run +`pip install dnspython[trio]`. + +If you want to use WMI on Windows to determine the active DNS settings +instead of the default registry scanning method, run +`pip install dnspython[wmi]`. + +If you want to try the experimental DNS-over-QUIC code, run +`pip install dnspython[doq]`. + +Note that you can install any combination of the above, e.g.: +`pip install dnspython[doh,dnssec,idna]` + +### Notices + +Python 2.x support ended with the release of 1.16.0. `dnspython` supports Python 3.10 +and later. Future support is aligned with the lifetime of the Python 3 versions. + +Documentation has moved to +[dnspython.readthedocs.io](https://dnspython.readthedocs.io). diff --git a/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/RECORD b/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/RECORD new file mode 100644 index 0000000..397075c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/RECORD @@ -0,0 +1,304 @@ +dns/__init__.py,sha256=2TTaN3FRnBIkYhrrkDUs7XYnu4h9zTlfOWdQ4nLuxnA,1693 +dns/__pycache__/__init__.cpython-311.pyc,, +dns/__pycache__/_asyncbackend.cpython-311.pyc,, +dns/__pycache__/_asyncio_backend.cpython-311.pyc,, +dns/__pycache__/_ddr.cpython-311.pyc,, +dns/__pycache__/_features.cpython-311.pyc,, +dns/__pycache__/_immutable_ctx.cpython-311.pyc,, +dns/__pycache__/_no_ssl.cpython-311.pyc,, +dns/__pycache__/_tls_util.cpython-311.pyc,, +dns/__pycache__/_trio_backend.cpython-311.pyc,, +dns/__pycache__/asyncbackend.cpython-311.pyc,, +dns/__pycache__/asyncquery.cpython-311.pyc,, +dns/__pycache__/asyncresolver.cpython-311.pyc,, +dns/__pycache__/btree.cpython-311.pyc,, +dns/__pycache__/btreezone.cpython-311.pyc,, +dns/__pycache__/dnssec.cpython-311.pyc,, +dns/__pycache__/dnssectypes.cpython-311.pyc,, +dns/__pycache__/e164.cpython-311.pyc,, +dns/__pycache__/edns.cpython-311.pyc,, +dns/__pycache__/entropy.cpython-311.pyc,, +dns/__pycache__/enum.cpython-311.pyc,, +dns/__pycache__/exception.cpython-311.pyc,, +dns/__pycache__/flags.cpython-311.pyc,, +dns/__pycache__/grange.cpython-311.pyc,, +dns/__pycache__/immutable.cpython-311.pyc,, +dns/__pycache__/inet.cpython-311.pyc,, +dns/__pycache__/ipv4.cpython-311.pyc,, +dns/__pycache__/ipv6.cpython-311.pyc,, +dns/__pycache__/message.cpython-311.pyc,, +dns/__pycache__/name.cpython-311.pyc,, +dns/__pycache__/namedict.cpython-311.pyc,, +dns/__pycache__/nameserver.cpython-311.pyc,, +dns/__pycache__/node.cpython-311.pyc,, +dns/__pycache__/opcode.cpython-311.pyc,, +dns/__pycache__/query.cpython-311.pyc,, +dns/__pycache__/rcode.cpython-311.pyc,, +dns/__pycache__/rdata.cpython-311.pyc,, +dns/__pycache__/rdataclass.cpython-311.pyc,, +dns/__pycache__/rdataset.cpython-311.pyc,, +dns/__pycache__/rdatatype.cpython-311.pyc,, +dns/__pycache__/renderer.cpython-311.pyc,, +dns/__pycache__/resolver.cpython-311.pyc,, +dns/__pycache__/reversename.cpython-311.pyc,, +dns/__pycache__/rrset.cpython-311.pyc,, +dns/__pycache__/serial.cpython-311.pyc,, +dns/__pycache__/set.cpython-311.pyc,, +dns/__pycache__/tokenizer.cpython-311.pyc,, +dns/__pycache__/transaction.cpython-311.pyc,, +dns/__pycache__/tsig.cpython-311.pyc,, +dns/__pycache__/tsigkeyring.cpython-311.pyc,, +dns/__pycache__/ttl.cpython-311.pyc,, +dns/__pycache__/update.cpython-311.pyc,, +dns/__pycache__/version.cpython-311.pyc,, +dns/__pycache__/versioned.cpython-311.pyc,, +dns/__pycache__/win32util.cpython-311.pyc,, +dns/__pycache__/wire.cpython-311.pyc,, +dns/__pycache__/xfr.cpython-311.pyc,, +dns/__pycache__/zone.cpython-311.pyc,, +dns/__pycache__/zonefile.cpython-311.pyc,, +dns/__pycache__/zonetypes.cpython-311.pyc,, +dns/_asyncbackend.py,sha256=bv-2iaDTEDH4Esx2tc2GeVCnaqHtsQqb3WWqoYZngzA,2403 +dns/_asyncio_backend.py,sha256=08Ezq3L8G190Sdr8qMgjwnWNhbyMa1MFB3pWYkGQ0a0,9147 +dns/_ddr.py,sha256=rHXKC8kncCTT9N4KBh1flicl79nyDjQ-DDvq30MJ3B8,5247 +dns/_features.py,sha256=VYTUetGL5x8IEtxMUQk9_ftat2cvyYJw8HfIfpMM8D8,2493 +dns/_immutable_ctx.py,sha256=Schj9tuGUAQ_QMh612H7Uq6XcvPo5AkVwoBxZJJ8liA,2478 +dns/_no_ssl.py,sha256=M8mj_xYkpsuhny_vHaTWCjI1pNvekYG6V52kdqFkUYY,1502 +dns/_tls_util.py,sha256=kcvrPdGnSGP1fP9sNKekBZ3j-599HwZkmAk6ybyCebM,528 +dns/_trio_backend.py,sha256=Tqzm46FuRSYkUJDYL8qp6Qk8hbc6ZxiLBc8z-NsTULg,8597 +dns/asyncbackend.py,sha256=82fXTFls_m7F_ekQbgUGOkoBbs4BI-GBLDZAWNGUvJ0,2796 +dns/asyncquery.py,sha256=34B1EIekX3oSg0jF8ZSqEiUbNZTsJa3r2oqC01OIY7U,32329 +dns/asyncresolver.py,sha256=TncJ7UukzA0vF79AwNa2gel0y9UO02tCdQf3zUHbygg,17728 +dns/btree.py,sha256=QPz4IzW_yTtSmz_DC6LKvZdJvTs50CQRKbAa0UAFMTs,30757 +dns/btreezone.py,sha256=H9orKjQaMhnPjtAhHpRZlV5wd91N17iuqOmTUVzv6sU,13082 +dns/dnssec.py,sha256=zXqhmUM4k6M-9YVR49crEI6Jc0zhZSk7NX9BWDafhTQ,41356 +dns/dnssecalgs/__init__.py,sha256=B4hebjElugf8zhCauhH6kvACqI50iYLSKxEqUfL6970,4350 +dns/dnssecalgs/__pycache__/__init__.cpython-311.pyc,, +dns/dnssecalgs/__pycache__/base.cpython-311.pyc,, +dns/dnssecalgs/__pycache__/cryptography.cpython-311.pyc,, +dns/dnssecalgs/__pycache__/dsa.cpython-311.pyc,, +dns/dnssecalgs/__pycache__/ecdsa.cpython-311.pyc,, +dns/dnssecalgs/__pycache__/eddsa.cpython-311.pyc,, +dns/dnssecalgs/__pycache__/rsa.cpython-311.pyc,, +dns/dnssecalgs/base.py,sha256=4Oq9EhKBEYupojZ3hENBiuq2Js3Spimy_NeDb9Rl1a8,2497 +dns/dnssecalgs/cryptography.py,sha256=utsBa_s8OOOKUeudvFullBNMRMjHmeoa66RNA6UiJMw,2428 +dns/dnssecalgs/dsa.py,sha256=ONilkD8Hhartj3Mwe7LKBT0vXS4E0KgfvTtV2ysZLhM,3605 +dns/dnssecalgs/ecdsa.py,sha256=TK8PclMAt7xVQTv6FIse9jZwXVCv_B-_AAgfhK0rTWQ,3283 +dns/dnssecalgs/eddsa.py,sha256=Yc0L9O2A_ySOSSalJiq5h7TU1LWtJgW1JIJWsGx96FI,2000 +dns/dnssecalgs/rsa.py,sha256=YOPPtpfOKdgBfBJvOcDofYTiC4mGmwCfqdYUvEbdHf8,3663 +dns/dnssectypes.py,sha256=CyeuGTS_rM3zXr8wD9qMT9jkzvVfTY2JWckUcogG83E,1799 +dns/e164.py,sha256=Sc-Ctv8lXpaDot_Su02wLFxLpxLReVW7_23YiGrnMC4,3937 +dns/edns.py,sha256=E5HRHMJNGGOyNvkR4iKY2jkaoQasa4K61Feuko9uY5s,17436 +dns/entropy.py,sha256=dSbsNoNVoypURvOu-clqMiD-dFQ-fsKOPYSHwoTjaec,4247 +dns/enum.py,sha256=PBphGzrIWOi8l3MgvkEMpsJapKIejkaQUqFuMWUcZXc,3685 +dns/exception.py,sha256=zEdlBUUsjb3dqk0etKxbFXUng0lLB7TPj7JFsNN7HzQ,5936 +dns/flags.py,sha256=cQ3kTFyvcKiWHAxI5AwchNqxVOrsIrgJ6brgrH42Wq8,2750 +dns/grange.py,sha256=ZqjNVDtb7i6E9D3ai6mcWR_nFNHyCXPp7j3dLFidtvY,2154 +dns/immutable.py,sha256=InrtpKvPxl-74oYbzsyneZwAuX78hUqeG22f2aniZbk,2017 +dns/inet.py,sha256=DbkUeb4PNLmxgUVPXX1GeWQH6e7a5WZ2AP_-befdg-o,5753 +dns/ipv4.py,sha256=dRiZRfyZAOlwlj3YlfbvZChRQAKstYh9k0ibNZwHu5U,2487 +dns/ipv6.py,sha256=GccOccOFZGFlwNFgV79GffZJv6u1GW28jM_amdiLqeM,6517 +dns/message.py,sha256=YVNQjYYFDSY6ttuwz_zvJnsCGuY1t11DdchsNlcBHG0,69152 +dns/name.py,sha256=rHvrUjhkCoR0_ANOH3fHJcY1swefx62SfBTDRvoGTsI,42910 +dns/namedict.py,sha256=hJRYpKeQv6Bd2LaUOPV0L_a0eXEIuqgggPXaH4c3Tow,4000 +dns/nameserver.py,sha256=LLOUGTjdAcj4cs-zAXeaH7Pf90IW0P64MQOrAb9PAPE,10007 +dns/node.py,sha256=Z2lzeqvPjqoR-Pbevp0OJqI_bGxwYzJIIevUccTElaM,12627 +dns/opcode.py,sha256=2EgPHQaGBRXN5q4C0KslagWbmWAbyT9Cw_cBj_sMXeA,2774 +dns/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +dns/query.py,sha256=85EWlMD1hDJO5xozZ7tFazMbZldpQ04L0sQFoQfBZiI,61686 +dns/quic/__init__.py,sha256=eqHPKj8SUk5rdeQxJSS-x3XSVqwcUPZlzTUio8mOpSg,2575 +dns/quic/__pycache__/__init__.cpython-311.pyc,, +dns/quic/__pycache__/_asyncio.cpython-311.pyc,, +dns/quic/__pycache__/_common.cpython-311.pyc,, +dns/quic/__pycache__/_sync.cpython-311.pyc,, +dns/quic/__pycache__/_trio.cpython-311.pyc,, +dns/quic/_asyncio.py,sha256=YgoU65THKtpHfV8UPAnNr-HkpbkR7XY01E7R3oh5apg,10314 +dns/quic/_common.py,sha256=M7lfxwUfr07fHkefo9BbRogQmwB_lEbittc7ZAQ_ulI,11087 +dns/quic/_sync.py,sha256=Ixj0BR6ngRWaKqTUiTrYbLw0rWVsUE6uJuNJB5oUlI0,10982 +dns/quic/_trio.py,sha256=NdClJJ80TY4kg8wM34JCfzX75fhhDb0vLy-WZkSyW6E,9452 +dns/rcode.py,sha256=A7UyvwbaFDz1PZaoYcAmXcerpZV-bRC2Zv3uJepiXa4,4181 +dns/rdata.py,sha256=7OAmPoSVEysCF84bjvaGXrfB1K69bpswaKtM1X89tXQ,31977 +dns/rdataclass.py,sha256=TK4W4ywB1L_X7EZqk2Gmwnu7vdQpolQF5DtQWyNk5xo,2984 +dns/rdataset.py,sha256=aoOatp7pbWhs2JieS0vcHnNc4dfwA0SBuvXAoqe3vxE,16627 +dns/rdatatype.py,sha256=W7r_B43ja4ZTHIJgqbb2eR99lXOYntf3ngGj396AvKg,7487 +dns/rdtypes/ANY/AFSDB.py,sha256=k75wMwreF1DAfDymu4lHh16BUx7ulVP3PLeQBZnkurY,1661 +dns/rdtypes/ANY/AMTRELAY.py,sha256=zE5xls02_NvbQwXUy-MnpV-uVVSJJuaKtZ86H8_X4ic,3355 +dns/rdtypes/ANY/AVC.py,sha256=SpsXYzlBirRWN0mGnQe0MdN6H8fvlgXPJX5PjOHnEak,1024 +dns/rdtypes/ANY/CAA.py,sha256=Hq1tHBrFW-BdxkjrGCq9u6ezaUHj6nFspBD5ClpkRYc,2456 +dns/rdtypes/ANY/CDNSKEY.py,sha256=bJAdrBMsFHIJz8TF1AxZoNbdxVWBCRTG-bR_uR_r_G4,1225 +dns/rdtypes/ANY/CDS.py,sha256=Y9nIRUCAabztVLbxm2SXAdYapFemCOUuGh5JqroCDUs,1163 +dns/rdtypes/ANY/CERT.py,sha256=OAYbtDdcwRhW8w_lbxHbgyWUHxYkTHV2zbiQff00X74,3547 +dns/rdtypes/ANY/CNAME.py,sha256=IHGGq2BDpeKUahTr1pvyBQgm0NGBI_vQ3Vs5mKTXO4w,1206 +dns/rdtypes/ANY/CSYNC.py,sha256=TnO2TjHfc9Cccfsz8dSsuH9Y53o-HllMVeU2DSAglrc,2431 +dns/rdtypes/ANY/DLV.py,sha256=J-pOrw5xXsDoaB9G0r6znlYXJtqtcqhsl1OXs6CPRU4,986 +dns/rdtypes/ANY/DNAME.py,sha256=yqXRtx4dAWwB4YCCv-qW6uaxeGhg2LPQ2uyKwWaMdXs,1150 +dns/rdtypes/ANY/DNSKEY.py,sha256=MD8HUVH5XXeAGOnFWg5aVz_w-2tXYwCeVXmzExhiIeQ,1223 +dns/rdtypes/ANY/DS.py,sha256=_gf8vk1O_uY8QXFjsfUw-bny-fm6e-QpCk3PT0JCyoM,995 +dns/rdtypes/ANY/DSYNC.py,sha256=q-26ceC4f2A2A6OmVaiOwDwAe_LAHvRsra1PZ4GyotA,2154 +dns/rdtypes/ANY/EUI48.py,sha256=x0BkK0sY_tgzuCwfDYpw6tyuChHjjtbRpAgYhO0Y44o,1151 +dns/rdtypes/ANY/EUI64.py,sha256=1jCff2-SXHJLDnNDnMW8Cd_o-ok0P3x6zKy_bcCU5h4,1161 +dns/rdtypes/ANY/GPOS.py,sha256=u4qwiDBVoC7bsKfxDKGbPjnOKddpdjy2p1AhziDWcPw,4439 +dns/rdtypes/ANY/HINFO.py,sha256=D2WvjTsvD_XqT8BepBIyjPL2iYGMgYqb1VQa9ApO0qE,2217 +dns/rdtypes/ANY/HIP.py,sha256=WSw31w96y1JM6ufasx7gRHUPTQuI5ejtyLxpD7vcINE,3216 +dns/rdtypes/ANY/ISDN.py,sha256=L4C2Rxrr4JJN17lmJRbZN8RhM_ujjwIskY_4V4Gd3r4,2723 +dns/rdtypes/ANY/L32.py,sha256=I0HcPHmvRUz2_yeDd0c5uueNKwcxmbz6V-7upNOc1GA,1302 +dns/rdtypes/ANY/L64.py,sha256=rbdYukNdezhQGH6vowKu1VbUWwi5cYSg_VbWEDWyYGA,1609 +dns/rdtypes/ANY/LOC.py,sha256=jxbB0bmbnMW8AVrElmoSW0SOmLPoEf5AwQLwUeAyMsY,11962 +dns/rdtypes/ANY/LP.py,sha256=X0xGo9vr1b3AQ8J8LPMyn_ooKRuEmjwdi7TGE2mqK_k,1332 +dns/rdtypes/ANY/MX.py,sha256=qQk83idY0-SbRMDmB15JOpJi7cSyiheF-ALUD0Ev19E,995 +dns/rdtypes/ANY/NID.py,sha256=8D8RDttb0BPObs0dXbFKajAhA05iZlqAq-51b6wusEI,1561 +dns/rdtypes/ANY/NINFO.py,sha256=bdL_-6Bejb2EH-xwR1rfSr_9E3SDXLTAnov7x2924FI,1041 +dns/rdtypes/ANY/NS.py,sha256=ThfaPalUlhbyZyNyvBM3k-7onl3eJKq5wCORrOGtkMM,995 +dns/rdtypes/ANY/NSEC.py,sha256=kicEYxcKaLBpV6C_M8cHdDaqBoiYl6EYtPvjyR6kExI,2465 +dns/rdtypes/ANY/NSEC3.py,sha256=NUG3AT626zu3My8QeNMiPVfpn3PRK9AGBkKW3cIZDzM,4250 +dns/rdtypes/ANY/NSEC3PARAM.py,sha256=-r5rBTMezSh7J9Wb7bWng_TXPKIETs2AXY4WFdhz7tM,2625 +dns/rdtypes/ANY/OPENPGPKEY.py,sha256=3LHryx1g0g-WrOI19PhGzGZG0anIJw2CCn93P4aT-Lk,1870 +dns/rdtypes/ANY/OPT.py,sha256=W36RslT_Psp95OPUC70knumOYjKpaRHvGT27I-NV2qc,2561 +dns/rdtypes/ANY/PTR.py,sha256=5HcR1D77Otyk91vVY4tmqrfZfSxSXWyWvwIW-rIH5gc,997 +dns/rdtypes/ANY/RESINFO.py,sha256=Kf2NcKbkeI5gFE1bJfQNqQCaitYyXfV_9nQYl1luUZ0,1008 +dns/rdtypes/ANY/RP.py,sha256=8doJlhjYDYiAT6KNF1mAaemJ20YJFUPvit8LOx4-I-U,2174 +dns/rdtypes/ANY/RRSIG.py,sha256=_ohbap8Dp_3VMU4w7ozVWGyFCtpm8A-l1F1wQiFZogA,4941 +dns/rdtypes/ANY/RT.py,sha256=2t9q3FZQ28iEyceeU25KU2Ur0T5JxELAu8BTwfOUgVw,1013 +dns/rdtypes/ANY/SMIMEA.py,sha256=6yjHuVDfIEodBU9wxbCGCDZ5cWYwyY6FCk-aq2VNU0s,222 +dns/rdtypes/ANY/SOA.py,sha256=tbbpP7RK2kpTTYCgdAWGCxlIMcX9U5MTOhz7vLP4p0I,3034 +dns/rdtypes/ANY/SPF.py,sha256=rA3Srs9ECQx-37lqm7Zf7aYmMpp_asv4tGS8_fSQ-CU,1022 +dns/rdtypes/ANY/SSHFP.py,sha256=F5vrZB-MAmeGJFAgEwRjXxgxerhoAd6kT9AcNNmkcF4,2550 +dns/rdtypes/ANY/TKEY.py,sha256=qvMJd0HGQF1wHGk1eWdITBVnAkj1oTHHbP5zSzV4cTc,4848 +dns/rdtypes/ANY/TLSA.py,sha256=cytzebS3W7FFr9qeJ9gFSHq_bOwUk9aRVlXWHfnVrRs,218 +dns/rdtypes/ANY/TSIG.py,sha256=4fNQJSNWZXUKZejCciwQuUJtTw2g-YbPmqHrEj_pitg,4750 +dns/rdtypes/ANY/TXT.py,sha256=F1U9gIAhwXIV4UVT7CwOCEn_su6G1nJIdgWJsLktk20,1000 +dns/rdtypes/ANY/URI.py,sha256=JyPYKh2RXzI34oABDiJ2oDh3TE_l-zmut4jBNA-ONt4,2913 +dns/rdtypes/ANY/WALLET.py,sha256=IaP2g7Nq26jWGKa8MVxvJjWXLQ0wrNR1IWJVyyMG8oU,219 +dns/rdtypes/ANY/X25.py,sha256=BzEM7uOY7CMAm7QN-dSLj-_LvgnnohwJDUjMstzwqYo,1942 +dns/rdtypes/ANY/ZONEMD.py,sha256=DjBYvHY13nF70uxTM77zf3R9n0Uy8Frbj1LuBXbC7jU,2389 +dns/rdtypes/ANY/__init__.py,sha256=2UKaYp81SLH6ofE021on9pR7jzmB47D1iXjQ3M7FXrw,1539 +dns/rdtypes/ANY/__pycache__/AFSDB.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/AMTRELAY.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/AVC.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/CAA.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/CDNSKEY.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/CDS.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/CERT.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/CNAME.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/CSYNC.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/DLV.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/DNAME.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/DNSKEY.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/DS.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/DSYNC.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/EUI48.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/EUI64.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/GPOS.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/HINFO.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/HIP.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/ISDN.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/L32.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/L64.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/LOC.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/LP.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/MX.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/NID.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/NINFO.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/NS.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/NSEC.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/NSEC3.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/NSEC3PARAM.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/OPENPGPKEY.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/OPT.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/PTR.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/RESINFO.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/RP.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/RRSIG.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/RT.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/SMIMEA.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/SOA.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/SPF.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/SSHFP.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/TKEY.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/TLSA.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/TSIG.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/TXT.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/URI.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/WALLET.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/X25.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/ZONEMD.cpython-311.pyc,, +dns/rdtypes/ANY/__pycache__/__init__.cpython-311.pyc,, +dns/rdtypes/CH/A.py,sha256=Iq82L3RLM-OwB5hyvtX1Das9oToiZMzNgs979cAkDz8,2229 +dns/rdtypes/CH/__init__.py,sha256=GD9YeDKb9VBDo-J5rrChX1MWEGyQXuR9Htnbhg_iYLc,923 +dns/rdtypes/CH/__pycache__/A.cpython-311.pyc,, +dns/rdtypes/CH/__pycache__/__init__.cpython-311.pyc,, +dns/rdtypes/IN/A.py,sha256=FfFn3SqbpneL9Ky63COP50V2ZFxqS1ldCKJh39Enwug,1814 +dns/rdtypes/IN/AAAA.py,sha256=AxrOlYy-1TTTWeQypDKeXrDCrdHGor0EKCE4fxzSQGo,1820 +dns/rdtypes/IN/APL.py,sha256=4Kz56antsRGu-cfV2MCHN8rmVo90wnZXnLWA6uQpnk4,5081 +dns/rdtypes/IN/DHCID.py,sha256=x9vedfzJ3vvxPC1ihWTTcxXBMYL0Q24Wmj6O67aY5og,1875 +dns/rdtypes/IN/HTTPS.py,sha256=P-IjwcvDQMmtoBgsDHglXF7KgLX73G6jEDqCKsnaGpQ,220 +dns/rdtypes/IN/IPSECKEY.py,sha256=jMO-aGl1eglWDqMxAkM2BvKDjfe9O1X0avBoWCtWi30,3261 +dns/rdtypes/IN/KX.py,sha256=K1JwItL0n5G-YGFCjWeh0C9DyDD8G8VzicsBeQiNAv0,1013 +dns/rdtypes/IN/NAPTR.py,sha256=JhGpvtCn_qlNWWlW9ilrWh9PNElBgNq1SWJPqD3LRzA,3741 +dns/rdtypes/IN/NSAP.py,sha256=6YfWCVSIPTTBmRAzG8nVBj3LnohncXUhSFJHgp-TRdc,2163 +dns/rdtypes/IN/NSAP_PTR.py,sha256=iTxlV6fr_Y9lqivLLncSHxEhmFqz5UEElDW3HMBtuCU,1015 +dns/rdtypes/IN/PX.py,sha256=zRg_5eGQdpzCRUsXIccxJOs7xoTAn7i4PIrj0Zwv-1A,2748 +dns/rdtypes/IN/SRV.py,sha256=TVai6Rtfx0_73wH999uPGuz-p2m6BTVIleXy1Tlm5Dc,2759 +dns/rdtypes/IN/SVCB.py,sha256=HeFmi2v01F00Hott8FlvQ4R7aPxFmT7RF-gt45R5K_M,218 +dns/rdtypes/IN/WKS.py,sha256=4_dLY3Bh6ePkfgku11QzLJv74iSyoSpt8EflIp_AMNc,3644 +dns/rdtypes/IN/__init__.py,sha256=HbI8aw9HWroI6SgEvl8Sx6FdkDswCCXMbSRuJy5o8LQ,1083 +dns/rdtypes/IN/__pycache__/A.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/AAAA.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/APL.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/DHCID.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/HTTPS.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/IPSECKEY.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/KX.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/NAPTR.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/NSAP.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/NSAP_PTR.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/PX.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/SRV.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/SVCB.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/WKS.cpython-311.pyc,, +dns/rdtypes/IN/__pycache__/__init__.cpython-311.pyc,, +dns/rdtypes/__init__.py,sha256=NYizfGglJfhqt_GMtSSXf7YQXIEHHCiJ_Y_qaLVeiOI,1073 +dns/rdtypes/__pycache__/__init__.cpython-311.pyc,, +dns/rdtypes/__pycache__/dnskeybase.cpython-311.pyc,, +dns/rdtypes/__pycache__/dsbase.cpython-311.pyc,, +dns/rdtypes/__pycache__/euibase.cpython-311.pyc,, +dns/rdtypes/__pycache__/mxbase.cpython-311.pyc,, +dns/rdtypes/__pycache__/nsbase.cpython-311.pyc,, +dns/rdtypes/__pycache__/svcbbase.cpython-311.pyc,, +dns/rdtypes/__pycache__/tlsabase.cpython-311.pyc,, +dns/rdtypes/__pycache__/txtbase.cpython-311.pyc,, +dns/rdtypes/__pycache__/util.cpython-311.pyc,, +dns/rdtypes/dnskeybase.py,sha256=GXSOvGtiRjY3fhqlI_T-4ukF4JQvvh3sk7UF0vipmPc,2824 +dns/rdtypes/dsbase.py,sha256=elOLkRb45vYzyh36_1FSJWWO9AI2wnK3GpddmQNdj3Y,3423 +dns/rdtypes/euibase.py,sha256=2DluC_kTi2io2ICgzFEdSxKGPFx3ib3ZXnA6YaAhAp0,2675 +dns/rdtypes/mxbase.py,sha256=N_3EX_2BgY0wMdGADL6_5nxBRUdx4ZcdNIYfGg5rMP8,3190 +dns/rdtypes/nsbase.py,sha256=tueXVV6E8lelebOmrmoOPq47eeRvOpsxHVXH4cOFxcs,2323 +dns/rdtypes/svcbbase.py,sha256=0VnPpt7fSCNt_MtGnWOiYtkY-6jQRWIli8JTRROakys,17717 +dns/rdtypes/tlsabase.py,sha256=hHuRO_MQ5g_tWBIDyTNArAWwbUc-MdZlXcjQxy5defA,2588 +dns/rdtypes/txtbase.py,sha256=lEzlKS6dx6UnhgoBPGIzqC3G0e8iWBetrkDtkwM16Ic,3723 +dns/rdtypes/util.py,sha256=WjiRlxsu_sq40XpSdR6wN54WWavKe7PLh-V9UaNhk7A,9680 +dns/renderer.py,sha256=sj_m9NRJoY8gdQ9zOhSVu0pTAUyBtM5AGpfea83jGpQ,11500 +dns/resolver.py,sha256=FRa-pJApeV_DFgLEwiwZP-2g7RHAg0kVCbg9EdNYLnc,73967 +dns/reversename.py,sha256=pPDGRfg7iq09cjEhKLKEcahdoyViS0y0ORip--r5vk8,3845 +dns/rrset.py,sha256=f8avzbtBb-y93jdyhhTJ8EJx1zOTcNTK3DtiK84eGNY,9129 +dns/serial.py,sha256=-t5rPW-TcJwzBMfIJo7Tl-uDtaYtpqOfCVYx9dMaDCY,3606 +dns/set.py,sha256=hublMKCIhd9zp5Hz_fvQTwF-Ze28jn7mjqei6vTGWfs,9213 +dns/tokenizer.py,sha256=dqQvBF3oUjP7URC7ZzBuQVLMVXhvf1gJusIpkV-IQ6U,23490 +dns/transaction.py,sha256=HnHa4nKL_ddtuWH4FaiKPEt81ImELL1fumZb3ll4KbI,22579 +dns/tsig.py,sha256=mWjZGZL75atl-jf3va1FhP9LfLGWT5g9Y9DgsSan4Mo,11576 +dns/tsigkeyring.py,sha256=1xSBgaV1KLR_9FQGsGWbkBD3XJjK8IFQx-H_olH1qyQ,2650 +dns/ttl.py,sha256=Rl8UOKV0_QyZzOdQ-JoB7nSHvBFehZXe_M0cxIBVc3Y,2937 +dns/update.py,sha256=iqZEO-_U0ooAqLlIRo1OhAKI8d-jpwPhBy-vC8v1dtY,12236 +dns/version.py,sha256=d7ViavUC8gYfrWbeyH8WMAldyGk_WVF5_zkCmCJv0ZQ,1763 +dns/versioned.py,sha256=yJ76QfKdIEKBtKX_DLA_IZGUZoFB1id1mMKzIj2eRm8,11841 +dns/win32util.py,sha256=iz5Gw0CTHAIqumdE25xdYUbhhSFiaZTRM-HXskglB2o,16799 +dns/wire.py,sha256=hylnQ30yjA3UcJSElhSAqYKMt5HICYqQ_N5b71K2smA,3155 +dns/xfr.py,sha256=UE4xAyfRDNH14x4os8yC-4Tl8brc_kCpBLxT0h6x-AM,13637 +dns/zone.py,sha256=ZferSA6wMN46uuBNkrgbRcSM8FSCCxMrNiLT3WoISbw,53098 +dns/zonefile.py,sha256=Xz24A8wH97NoA_iTbastSzUZ-S-DmLFG0SgIfVzQinY,28517 +dns/zonetypes.py,sha256=HrQNZxZ_gWLWI9dskix71msi9wkYK5pgrBBbPb1T74Y,690 +dnspython-2.8.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +dnspython-2.8.0.dist-info/METADATA,sha256=dPdZU5uJ4pkVGy1pfGEjBzRbdm27fpQ1z4Y6Bpgf04U,5680 +dnspython-2.8.0.dist-info/RECORD,, +dnspython-2.8.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +dnspython-2.8.0.dist-info/licenses/LICENSE,sha256=w-o_9WVLMpwZ07xfdIGvYjw93tSmFFWFSZ-EOtPXQc0,1526 diff --git a/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/WHEEL b/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/WHEEL new file mode 100644 index 0000000..12228d4 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/licenses/LICENSE b/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..390a726 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/licenses/LICENSE @@ -0,0 +1,35 @@ +ISC License + +Copyright (C) Dnspython Contributors + +Permission to use, copy, modify, and/or distribute this software for +any purpose with or without fee is hereby granted, provided that the +above copyright notice and this permission notice appear in all +copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL +WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE +AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL +DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR +PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER +TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +PERFORMANCE OF THIS SOFTWARE. + + + +Copyright (C) 2001-2017 Nominum, Inc. +Copyright (C) Google Inc. + +Permission to use, copy, modify, and distribute this software and its +documentation for any purpose with or without fee is hereby granted, +provided that the above copyright notice and this permission notice +appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND NOMINUM DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL NOMINUM BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT +OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/netdeploy/lib/python3.11/site-packages/dotenv/__init__.py b/netdeploy/lib/python3.11/site-packages/dotenv/__init__.py new file mode 100644 index 0000000..7f4c631 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dotenv/__init__.py @@ -0,0 +1,49 @@ +from typing import Any, Optional + +from .main import (dotenv_values, find_dotenv, get_key, load_dotenv, set_key, + unset_key) + + +def load_ipython_extension(ipython: Any) -> None: + from .ipython import load_ipython_extension + load_ipython_extension(ipython) + + +def get_cli_string( + path: Optional[str] = None, + action: Optional[str] = None, + key: Optional[str] = None, + value: Optional[str] = None, + quote: Optional[str] = None, +): + """Returns a string suitable for running as a shell script. + + Useful for converting a arguments passed to a fabric task + to be passed to a `local` or `run` command. + """ + command = ['dotenv'] + if quote: + command.append(f'-q {quote}') + if path: + command.append(f'-f {path}') + if action: + command.append(action) + if key: + command.append(key) + if value: + if ' ' in value: + command.append(f'"{value}"') + else: + command.append(value) + + return ' '.join(command).strip() + + +__all__ = ['get_cli_string', + 'load_dotenv', + 'dotenv_values', + 'get_key', + 'set_key', + 'unset_key', + 'find_dotenv', + 'load_ipython_extension'] diff --git a/netdeploy/lib/python3.11/site-packages/dotenv/__main__.py b/netdeploy/lib/python3.11/site-packages/dotenv/__main__.py new file mode 100644 index 0000000..3977f55 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dotenv/__main__.py @@ -0,0 +1,6 @@ +"""Entry point for cli, enables execution with `python -m dotenv`""" + +from .cli import cli + +if __name__ == "__main__": + cli() diff --git a/netdeploy/lib/python3.11/site-packages/dotenv/cli.py b/netdeploy/lib/python3.11/site-packages/dotenv/cli.py new file mode 100644 index 0000000..65ead46 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dotenv/cli.py @@ -0,0 +1,199 @@ +import json +import os +import shlex +import sys +from contextlib import contextmanager +from subprocess import Popen +from typing import Any, Dict, IO, Iterator, List + +try: + import click +except ImportError: + sys.stderr.write('It seems python-dotenv is not installed with cli option. \n' + 'Run pip install "python-dotenv[cli]" to fix this.') + sys.exit(1) + +from .main import dotenv_values, set_key, unset_key +from .version import __version__ + + +def enumerate_env(): + """ + Return a path for the ${pwd}/.env file. + + If pwd does not exist, return None. + """ + try: + cwd = os.getcwd() + except FileNotFoundError: + return None + path = os.path.join(cwd, '.env') + return path + + +@click.group() +@click.option('-f', '--file', default=enumerate_env(), + type=click.Path(file_okay=True), + help="Location of the .env file, defaults to .env file in current working directory.") +@click.option('-q', '--quote', default='always', + type=click.Choice(['always', 'never', 'auto']), + help="Whether to quote or not the variable values. Default mode is always. This does not affect parsing.") +@click.option('-e', '--export', default=False, + type=click.BOOL, + help="Whether to write the dot file as an executable bash script.") +@click.version_option(version=__version__) +@click.pass_context +def cli(ctx: click.Context, file: Any, quote: Any, export: Any) -> None: + """This script is used to set, get or unset values from a .env file.""" + ctx.obj = {'QUOTE': quote, 'EXPORT': export, 'FILE': file} + + +@contextmanager +def stream_file(path: os.PathLike) -> Iterator[IO[str]]: + """ + Open a file and yield the corresponding (decoded) stream. + + Exits with error code 2 if the file cannot be opened. + """ + + try: + with open(path) as stream: + yield stream + except OSError as exc: + print(f"Error opening env file: {exc}", file=sys.stderr) + exit(2) + + +@cli.command() +@click.pass_context +@click.option('--format', default='simple', + type=click.Choice(['simple', 'json', 'shell', 'export']), + help="The format in which to display the list. Default format is simple, " + "which displays name=value without quotes.") +def list(ctx: click.Context, format: bool) -> None: + """Display all the stored key/value.""" + file = ctx.obj['FILE'] + + with stream_file(file) as stream: + values = dotenv_values(stream=stream) + + if format == 'json': + click.echo(json.dumps(values, indent=2, sort_keys=True)) + else: + prefix = 'export ' if format == 'export' else '' + for k in sorted(values): + v = values[k] + if v is not None: + if format in ('export', 'shell'): + v = shlex.quote(v) + click.echo(f'{prefix}{k}={v}') + + +@cli.command() +@click.pass_context +@click.argument('key', required=True) +@click.argument('value', required=True) +def set(ctx: click.Context, key: Any, value: Any) -> None: + """Store the given key/value.""" + file = ctx.obj['FILE'] + quote = ctx.obj['QUOTE'] + export = ctx.obj['EXPORT'] + success, key, value = set_key(file, key, value, quote, export) + if success: + click.echo(f'{key}={value}') + else: + exit(1) + + +@cli.command() +@click.pass_context +@click.argument('key', required=True) +def get(ctx: click.Context, key: Any) -> None: + """Retrieve the value for the given key.""" + file = ctx.obj['FILE'] + + with stream_file(file) as stream: + values = dotenv_values(stream=stream) + + stored_value = values.get(key) + if stored_value: + click.echo(stored_value) + else: + exit(1) + + +@cli.command() +@click.pass_context +@click.argument('key', required=True) +def unset(ctx: click.Context, key: Any) -> None: + """Removes the given key.""" + file = ctx.obj['FILE'] + quote = ctx.obj['QUOTE'] + success, key = unset_key(file, key, quote) + if success: + click.echo(f"Successfully removed {key}") + else: + exit(1) + + +@cli.command(context_settings={'ignore_unknown_options': True}) +@click.pass_context +@click.option( + "--override/--no-override", + default=True, + help="Override variables from the environment file with those from the .env file.", +) +@click.argument('commandline', nargs=-1, type=click.UNPROCESSED) +def run(ctx: click.Context, override: bool, commandline: List[str]) -> None: + """Run command with environment variables present.""" + file = ctx.obj['FILE'] + if not os.path.isfile(file): + raise click.BadParameter( + f'Invalid value for \'-f\' "{file}" does not exist.', + ctx=ctx + ) + dotenv_as_dict = { + k: v + for (k, v) in dotenv_values(file).items() + if v is not None and (override or k not in os.environ) + } + + if not commandline: + click.echo('No command given.') + exit(1) + ret = run_command(commandline, dotenv_as_dict) + exit(ret) + + +def run_command(command: List[str], env: Dict[str, str]) -> int: + """Run command in sub process. + + Runs the command in a sub process with the variables from `env` + added in the current environment variables. + + Parameters + ---------- + command: List[str] + The command and it's parameters + env: Dict + The additional environment variables + + Returns + ------- + int + The return code of the command + + """ + # copy the current environment variables and add the vales from + # `env` + cmd_env = os.environ.copy() + cmd_env.update(env) + + p = Popen(command, + universal_newlines=True, + bufsize=0, + shell=False, + env=cmd_env) + _, _ = p.communicate() + + return p.returncode diff --git a/netdeploy/lib/python3.11/site-packages/dotenv/ipython.py b/netdeploy/lib/python3.11/site-packages/dotenv/ipython.py new file mode 100644 index 0000000..7df727c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dotenv/ipython.py @@ -0,0 +1,39 @@ +from IPython.core.magic import Magics, line_magic, magics_class # type: ignore +from IPython.core.magic_arguments import (argument, magic_arguments, # type: ignore + parse_argstring) # type: ignore + +from .main import find_dotenv, load_dotenv + + +@magics_class +class IPythonDotEnv(Magics): + + @magic_arguments() + @argument( + '-o', '--override', action='store_true', + help="Indicate to override existing variables" + ) + @argument( + '-v', '--verbose', action='store_true', + help="Indicate function calls to be verbose" + ) + @argument('dotenv_path', nargs='?', type=str, default='.env', + help='Search in increasingly higher folders for the `dotenv_path`') + @line_magic + def dotenv(self, line): + args = parse_argstring(self.dotenv, line) + # Locate the .env file + dotenv_path = args.dotenv_path + try: + dotenv_path = find_dotenv(dotenv_path, True, True) + except IOError: + print("cannot find .env file") + return + + # Load the .env file + load_dotenv(dotenv_path, verbose=args.verbose, override=args.override) + + +def load_ipython_extension(ipython): + """Register the %dotenv magic.""" + ipython.register_magics(IPythonDotEnv) diff --git a/netdeploy/lib/python3.11/site-packages/dotenv/main.py b/netdeploy/lib/python3.11/site-packages/dotenv/main.py new file mode 100644 index 0000000..7bc5428 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dotenv/main.py @@ -0,0 +1,392 @@ +import io +import logging +import os +import pathlib +import shutil +import sys +import tempfile +from collections import OrderedDict +from contextlib import contextmanager +from typing import (IO, Dict, Iterable, Iterator, Mapping, Optional, Tuple, + Union) + +from .parser import Binding, parse_stream +from .variables import parse_variables + +# A type alias for a string path to be used for the paths in this file. +# These paths may flow to `open()` and `shutil.move()`; `shutil.move()` +# only accepts string paths, not byte paths or file descriptors. See +# https://github.com/python/typeshed/pull/6832. +StrPath = Union[str, 'os.PathLike[str]'] + +logger = logging.getLogger(__name__) + + +def with_warn_for_invalid_lines(mappings: Iterator[Binding]) -> Iterator[Binding]: + for mapping in mappings: + if mapping.error: + logger.warning( + "Python-dotenv could not parse statement starting at line %s", + mapping.original.line, + ) + yield mapping + + +class DotEnv: + def __init__( + self, + dotenv_path: Optional[StrPath], + stream: Optional[IO[str]] = None, + verbose: bool = False, + encoding: Optional[str] = None, + interpolate: bool = True, + override: bool = True, + ) -> None: + self.dotenv_path: Optional[StrPath] = dotenv_path + self.stream: Optional[IO[str]] = stream + self._dict: Optional[Dict[str, Optional[str]]] = None + self.verbose: bool = verbose + self.encoding: Optional[str] = encoding + self.interpolate: bool = interpolate + self.override: bool = override + + @contextmanager + def _get_stream(self) -> Iterator[IO[str]]: + if self.dotenv_path and os.path.isfile(self.dotenv_path): + with open(self.dotenv_path, encoding=self.encoding) as stream: + yield stream + elif self.stream is not None: + yield self.stream + else: + if self.verbose: + logger.info( + "Python-dotenv could not find configuration file %s.", + self.dotenv_path or '.env', + ) + yield io.StringIO('') + + def dict(self) -> Dict[str, Optional[str]]: + """Return dotenv as dict""" + if self._dict: + return self._dict + + raw_values = self.parse() + + if self.interpolate: + self._dict = OrderedDict(resolve_variables(raw_values, override=self.override)) + else: + self._dict = OrderedDict(raw_values) + + return self._dict + + def parse(self) -> Iterator[Tuple[str, Optional[str]]]: + with self._get_stream() as stream: + for mapping in with_warn_for_invalid_lines(parse_stream(stream)): + if mapping.key is not None: + yield mapping.key, mapping.value + + def set_as_environment_variables(self) -> bool: + """ + Load the current dotenv as system environment variable. + """ + if not self.dict(): + return False + + for k, v in self.dict().items(): + if k in os.environ and not self.override: + continue + if v is not None: + os.environ[k] = v + + return True + + def get(self, key: str) -> Optional[str]: + """ + """ + data = self.dict() + + if key in data: + return data[key] + + if self.verbose: + logger.warning("Key %s not found in %s.", key, self.dotenv_path) + + return None + + +def get_key( + dotenv_path: StrPath, + key_to_get: str, + encoding: Optional[str] = "utf-8", +) -> Optional[str]: + """ + Get the value of a given key from the given .env. + + Returns `None` if the key isn't found or doesn't have a value. + """ + return DotEnv(dotenv_path, verbose=True, encoding=encoding).get(key_to_get) + + +@contextmanager +def rewrite( + path: StrPath, + encoding: Optional[str], +) -> Iterator[Tuple[IO[str], IO[str]]]: + pathlib.Path(path).touch() + + with tempfile.NamedTemporaryFile(mode="w", encoding=encoding, delete=False) as dest: + error = None + try: + with open(path, encoding=encoding) as source: + yield (source, dest) + except BaseException as err: + error = err + + if error is None: + shutil.move(dest.name, path) + else: + os.unlink(dest.name) + raise error from None + + +def set_key( + dotenv_path: StrPath, + key_to_set: str, + value_to_set: str, + quote_mode: str = "always", + export: bool = False, + encoding: Optional[str] = "utf-8", +) -> Tuple[Optional[bool], str, str]: + """ + Adds or Updates a key/value to the given .env + + If the .env path given doesn't exist, fails instead of risking creating + an orphan .env somewhere in the filesystem + """ + if quote_mode not in ("always", "auto", "never"): + raise ValueError(f"Unknown quote_mode: {quote_mode}") + + quote = ( + quote_mode == "always" + or (quote_mode == "auto" and not value_to_set.isalnum()) + ) + + if quote: + value_out = "'{}'".format(value_to_set.replace("'", "\\'")) + else: + value_out = value_to_set + if export: + line_out = f'export {key_to_set}={value_out}\n' + else: + line_out = f"{key_to_set}={value_out}\n" + + with rewrite(dotenv_path, encoding=encoding) as (source, dest): + replaced = False + missing_newline = False + for mapping in with_warn_for_invalid_lines(parse_stream(source)): + if mapping.key == key_to_set: + dest.write(line_out) + replaced = True + else: + dest.write(mapping.original.string) + missing_newline = not mapping.original.string.endswith("\n") + if not replaced: + if missing_newline: + dest.write("\n") + dest.write(line_out) + + return True, key_to_set, value_to_set + + +def unset_key( + dotenv_path: StrPath, + key_to_unset: str, + quote_mode: str = "always", + encoding: Optional[str] = "utf-8", +) -> Tuple[Optional[bool], str]: + """ + Removes a given key from the given `.env` file. + + If the .env path given doesn't exist, fails. + If the given key doesn't exist in the .env, fails. + """ + if not os.path.exists(dotenv_path): + logger.warning("Can't delete from %s - it doesn't exist.", dotenv_path) + return None, key_to_unset + + removed = False + with rewrite(dotenv_path, encoding=encoding) as (source, dest): + for mapping in with_warn_for_invalid_lines(parse_stream(source)): + if mapping.key == key_to_unset: + removed = True + else: + dest.write(mapping.original.string) + + if not removed: + logger.warning("Key %s not removed from %s - key doesn't exist.", key_to_unset, dotenv_path) + return None, key_to_unset + + return removed, key_to_unset + + +def resolve_variables( + values: Iterable[Tuple[str, Optional[str]]], + override: bool, +) -> Mapping[str, Optional[str]]: + new_values: Dict[str, Optional[str]] = {} + + for (name, value) in values: + if value is None: + result = None + else: + atoms = parse_variables(value) + env: Dict[str, Optional[str]] = {} + if override: + env.update(os.environ) # type: ignore + env.update(new_values) + else: + env.update(new_values) + env.update(os.environ) # type: ignore + result = "".join(atom.resolve(env) for atom in atoms) + + new_values[name] = result + + return new_values + + +def _walk_to_root(path: str) -> Iterator[str]: + """ + Yield directories starting from the given directory up to the root + """ + if not os.path.exists(path): + raise IOError('Starting path not found') + + if os.path.isfile(path): + path = os.path.dirname(path) + + last_dir = None + current_dir = os.path.abspath(path) + while last_dir != current_dir: + yield current_dir + parent_dir = os.path.abspath(os.path.join(current_dir, os.path.pardir)) + last_dir, current_dir = current_dir, parent_dir + + +def find_dotenv( + filename: str = '.env', + raise_error_if_not_found: bool = False, + usecwd: bool = False, +) -> str: + """ + Search in increasingly higher folders for the given file + + Returns path to the file if found, or an empty string otherwise + """ + + def _is_interactive(): + """ Decide whether this is running in a REPL or IPython notebook """ + try: + main = __import__('__main__', None, None, fromlist=['__file__']) + except ModuleNotFoundError: + return False + return not hasattr(main, '__file__') + + if usecwd or _is_interactive() or getattr(sys, 'frozen', False): + # Should work without __file__, e.g. in REPL or IPython notebook. + path = os.getcwd() + else: + # will work for .py files + frame = sys._getframe() + current_file = __file__ + + while frame.f_code.co_filename == current_file or not os.path.exists( + frame.f_code.co_filename + ): + assert frame.f_back is not None + frame = frame.f_back + frame_filename = frame.f_code.co_filename + path = os.path.dirname(os.path.abspath(frame_filename)) + + for dirname in _walk_to_root(path): + check_path = os.path.join(dirname, filename) + if os.path.isfile(check_path): + return check_path + + if raise_error_if_not_found: + raise IOError('File not found') + + return '' + + +def load_dotenv( + dotenv_path: Optional[StrPath] = None, + stream: Optional[IO[str]] = None, + verbose: bool = False, + override: bool = False, + interpolate: bool = True, + encoding: Optional[str] = "utf-8", +) -> bool: + """Parse a .env file and then load all the variables found as environment variables. + + Parameters: + dotenv_path: Absolute or relative path to .env file. + stream: Text stream (such as `io.StringIO`) with .env content, used if + `dotenv_path` is `None`. + verbose: Whether to output a warning the .env file is missing. + override: Whether to override the system environment variables with the variables + from the `.env` file. + encoding: Encoding to be used to read the file. + Returns: + Bool: True if at least one environment variable is set else False + + If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the + .env file. + """ + if dotenv_path is None and stream is None: + dotenv_path = find_dotenv() + + dotenv = DotEnv( + dotenv_path=dotenv_path, + stream=stream, + verbose=verbose, + interpolate=interpolate, + override=override, + encoding=encoding, + ) + return dotenv.set_as_environment_variables() + + +def dotenv_values( + dotenv_path: Optional[StrPath] = None, + stream: Optional[IO[str]] = None, + verbose: bool = False, + interpolate: bool = True, + encoding: Optional[str] = "utf-8", +) -> Dict[str, Optional[str]]: + """ + Parse a .env file and return its content as a dict. + + The returned dict will have `None` values for keys without values in the .env file. + For example, `foo=bar` results in `{"foo": "bar"}` whereas `foo` alone results in + `{"foo": None}` + + Parameters: + dotenv_path: Absolute or relative path to the .env file. + stream: `StringIO` object with .env content, used if `dotenv_path` is `None`. + verbose: Whether to output a warning if the .env file is missing. + encoding: Encoding to be used to read the file. + + If both `dotenv_path` and `stream` are `None`, `find_dotenv()` is used to find the + .env file. + """ + if dotenv_path is None and stream is None: + dotenv_path = find_dotenv() + + return DotEnv( + dotenv_path=dotenv_path, + stream=stream, + verbose=verbose, + interpolate=interpolate, + override=True, + encoding=encoding, + ).dict() diff --git a/netdeploy/lib/python3.11/site-packages/dotenv/parser.py b/netdeploy/lib/python3.11/site-packages/dotenv/parser.py new file mode 100644 index 0000000..735f14a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dotenv/parser.py @@ -0,0 +1,175 @@ +import codecs +import re +from typing import (IO, Iterator, Match, NamedTuple, Optional, # noqa:F401 + Pattern, Sequence, Tuple) + + +def make_regex(string: str, extra_flags: int = 0) -> Pattern[str]: + return re.compile(string, re.UNICODE | extra_flags) + + +_newline = make_regex(r"(\r\n|\n|\r)") +_multiline_whitespace = make_regex(r"\s*", extra_flags=re.MULTILINE) +_whitespace = make_regex(r"[^\S\r\n]*") +_export = make_regex(r"(?:export[^\S\r\n]+)?") +_single_quoted_key = make_regex(r"'([^']+)'") +_unquoted_key = make_regex(r"([^=\#\s]+)") +_equal_sign = make_regex(r"(=[^\S\r\n]*)") +_single_quoted_value = make_regex(r"'((?:\\'|[^'])*)'") +_double_quoted_value = make_regex(r'"((?:\\"|[^"])*)"') +_unquoted_value = make_regex(r"([^\r\n]*)") +_comment = make_regex(r"(?:[^\S\r\n]*#[^\r\n]*)?") +_end_of_line = make_regex(r"[^\S\r\n]*(?:\r\n|\n|\r|$)") +_rest_of_line = make_regex(r"[^\r\n]*(?:\r|\n|\r\n)?") +_double_quote_escapes = make_regex(r"\\[\\'\"abfnrtv]") +_single_quote_escapes = make_regex(r"\\[\\']") + + +class Original(NamedTuple): + string: str + line: int + + +class Binding(NamedTuple): + key: Optional[str] + value: Optional[str] + original: Original + error: bool + + +class Position: + def __init__(self, chars: int, line: int) -> None: + self.chars = chars + self.line = line + + @classmethod + def start(cls) -> "Position": + return cls(chars=0, line=1) + + def set(self, other: "Position") -> None: + self.chars = other.chars + self.line = other.line + + def advance(self, string: str) -> None: + self.chars += len(string) + self.line += len(re.findall(_newline, string)) + + +class Error(Exception): + pass + + +class Reader: + def __init__(self, stream: IO[str]) -> None: + self.string = stream.read() + self.position = Position.start() + self.mark = Position.start() + + def has_next(self) -> bool: + return self.position.chars < len(self.string) + + def set_mark(self) -> None: + self.mark.set(self.position) + + def get_marked(self) -> Original: + return Original( + string=self.string[self.mark.chars:self.position.chars], + line=self.mark.line, + ) + + def peek(self, count: int) -> str: + return self.string[self.position.chars:self.position.chars + count] + + def read(self, count: int) -> str: + result = self.string[self.position.chars:self.position.chars + count] + if len(result) < count: + raise Error("read: End of string") + self.position.advance(result) + return result + + def read_regex(self, regex: Pattern[str]) -> Sequence[str]: + match = regex.match(self.string, self.position.chars) + if match is None: + raise Error("read_regex: Pattern not found") + self.position.advance(self.string[match.start():match.end()]) + return match.groups() + + +def decode_escapes(regex: Pattern[str], string: str) -> str: + def decode_match(match: Match[str]) -> str: + return codecs.decode(match.group(0), 'unicode-escape') # type: ignore + + return regex.sub(decode_match, string) + + +def parse_key(reader: Reader) -> Optional[str]: + char = reader.peek(1) + if char == "#": + return None + elif char == "'": + (key,) = reader.read_regex(_single_quoted_key) + else: + (key,) = reader.read_regex(_unquoted_key) + return key + + +def parse_unquoted_value(reader: Reader) -> str: + (part,) = reader.read_regex(_unquoted_value) + return re.sub(r"\s+#.*", "", part).rstrip() + + +def parse_value(reader: Reader) -> str: + char = reader.peek(1) + if char == u"'": + (value,) = reader.read_regex(_single_quoted_value) + return decode_escapes(_single_quote_escapes, value) + elif char == u'"': + (value,) = reader.read_regex(_double_quoted_value) + return decode_escapes(_double_quote_escapes, value) + elif char in (u"", u"\n", u"\r"): + return u"" + else: + return parse_unquoted_value(reader) + + +def parse_binding(reader: Reader) -> Binding: + reader.set_mark() + try: + reader.read_regex(_multiline_whitespace) + if not reader.has_next(): + return Binding( + key=None, + value=None, + original=reader.get_marked(), + error=False, + ) + reader.read_regex(_export) + key = parse_key(reader) + reader.read_regex(_whitespace) + if reader.peek(1) == "=": + reader.read_regex(_equal_sign) + value: Optional[str] = parse_value(reader) + else: + value = None + reader.read_regex(_comment) + reader.read_regex(_end_of_line) + return Binding( + key=key, + value=value, + original=reader.get_marked(), + error=False, + ) + except Error: + reader.read_regex(_rest_of_line) + return Binding( + key=None, + value=None, + original=reader.get_marked(), + error=True, + ) + + +def parse_stream(stream: IO[str]) -> Iterator[Binding]: + reader = Reader(stream) + while reader.has_next(): + yield parse_binding(reader) diff --git a/netdeploy/lib/python3.11/site-packages/dotenv/py.typed b/netdeploy/lib/python3.11/site-packages/dotenv/py.typed new file mode 100644 index 0000000..7632ecf --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dotenv/py.typed @@ -0,0 +1 @@ +# Marker file for PEP 561 diff --git a/netdeploy/lib/python3.11/site-packages/dotenv/variables.py b/netdeploy/lib/python3.11/site-packages/dotenv/variables.py new file mode 100644 index 0000000..667f2f2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dotenv/variables.py @@ -0,0 +1,86 @@ +import re +from abc import ABCMeta, abstractmethod +from typing import Iterator, Mapping, Optional, Pattern + +_posix_variable: Pattern[str] = re.compile( + r""" + \$\{ + (?P[^\}:]*) + (?::- + (?P[^\}]*) + )? + \} + """, + re.VERBOSE, +) + + +class Atom(metaclass=ABCMeta): + def __ne__(self, other: object) -> bool: + result = self.__eq__(other) + if result is NotImplemented: + return NotImplemented + return not result + + @abstractmethod + def resolve(self, env: Mapping[str, Optional[str]]) -> str: ... + + +class Literal(Atom): + def __init__(self, value: str) -> None: + self.value = value + + def __repr__(self) -> str: + return f"Literal(value={self.value})" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + return self.value == other.value + + def __hash__(self) -> int: + return hash((self.__class__, self.value)) + + def resolve(self, env: Mapping[str, Optional[str]]) -> str: + return self.value + + +class Variable(Atom): + def __init__(self, name: str, default: Optional[str]) -> None: + self.name = name + self.default = default + + def __repr__(self) -> str: + return f"Variable(name={self.name}, default={self.default})" + + def __eq__(self, other: object) -> bool: + if not isinstance(other, self.__class__): + return NotImplemented + return (self.name, self.default) == (other.name, other.default) + + def __hash__(self) -> int: + return hash((self.__class__, self.name, self.default)) + + def resolve(self, env: Mapping[str, Optional[str]]) -> str: + default = self.default if self.default is not None else "" + result = env.get(self.name, default) + return result if result is not None else "" + + +def parse_variables(value: str) -> Iterator[Atom]: + cursor = 0 + + for match in _posix_variable.finditer(value): + (start, end) = match.span() + name = match["name"] + default = match["default"] + + if start > cursor: + yield Literal(value=value[cursor:start]) + + yield Variable(name=name, default=default) + cursor = end + + length = len(value) + if cursor < length: + yield Literal(value=value[cursor:length]) diff --git a/netdeploy/lib/python3.11/site-packages/dotenv/version.py b/netdeploy/lib/python3.11/site-packages/dotenv/version.py new file mode 100644 index 0000000..5c4105c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/dotenv/version.py @@ -0,0 +1 @@ +__version__ = "1.0.1" diff --git a/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/INSTALLER b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/METADATA b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/METADATA new file mode 100644 index 0000000..25ce6f6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/METADATA @@ -0,0 +1,129 @@ +Metadata-Version: 2.4 +Name: eventlet +Version: 0.40.3 +Summary: Highly concurrent networking library +Project-URL: Homepage, https://github.com/eventlet/eventlet +Project-URL: History, https://github.com/eventlet/eventlet/blob/master/NEWS +Project-URL: Tracker, https://github.com/eventlet/eventlet/issues +Project-URL: Source, https://github.com/eventlet/eventlet +Project-URL: Documentation, https://eventlet.readthedocs.io/ +Author-email: Sergey Shepelev , Jakub Stasiak , Tim Burke , Nat Goodspeed , Itamar Turner-Trauring , Hervé Beraud +License: MIT +License-File: AUTHORS +License-File: LICENSE +Classifier: Development Status :: 4 - Beta +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: Microsoft :: Windows +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +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: Programming Language :: Python :: 3.13 +Classifier: Topic :: Internet +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.9 +Requires-Dist: dnspython>=1.15.0 +Requires-Dist: greenlet>=1.0 +Provides-Extra: dev +Requires-Dist: black; extra == 'dev' +Requires-Dist: build; extra == 'dev' +Requires-Dist: commitizen; extra == 'dev' +Requires-Dist: isort; extra == 'dev' +Requires-Dist: pip-tools; extra == 'dev' +Requires-Dist: pre-commit; extra == 'dev' +Requires-Dist: twine; extra == 'dev' +Description-Content-Type: text/x-rst + +Warning +======= + +**New usages of eventlet are now heavily discouraged! Please read the +following.** + +Eventlet was created almost 18 years ago, at a time where async +features were absent from the CPython stdlib. With time eventlet evolved and +CPython too, but since several years the maintenance activity of eventlet +decreased leading to a growing gap between eventlet and the CPython +implementation. + +This gap is now too high and can lead you to unexpected side effects and bugs +in your applications. + +Eventlet now follows a new maintenance policy. **Only maintenance for +stability and bug fixing** will be provided. **No new features will be +accepted**, except those related to the asyncio migration. **Usages in new +projects are discouraged**. **Our goal is to plan the retirement of eventlet** +and to give you ways to move away from eventlet. + +If you are looking for a library to manage async network programming, +and if you do not yet use eventlet, then, we encourage you to use `asyncio`_, +which is the official async library of the CPython stdlib. + +If you already use eventlet, we hope to enable migration to asyncio for some use +cases; see `Migrating off of Eventlet`_. Only new features related to the migration +solution will be accepted. + +If you have questions concerning maintenance goals or concerning +the migration do not hesitate to `open a new issue`_, we will be happy to +answer them. + +.. _asyncio: https://docs.python.org/3/library/asyncio.html +.. _open a new issue: https://github.com/eventlet/eventlet/issues/new +.. _Migrating off of Eventlet: https://eventlet.readthedocs.io/en/latest/asyncio/migration.html#migration-guide + +Eventlet +======== + +.. image:: https://img.shields.io/pypi/v/eventlet + :target: https://pypi.org/project/eventlet/ + +.. image:: https://img.shields.io/github/actions/workflow/status/eventlet/eventlet/test.yaml?branch=master + :target: https://github.com/eventlet/eventlet/actions?query=workflow%3Atest+branch%3Amaster + +.. image:: https://codecov.io/gh/eventlet/eventlet/branch/master/graph/badge.svg + :target: https://codecov.io/gh/eventlet/eventlet + + +Eventlet is a concurrent networking library for Python that allows you to change how you run your code, not how you write it. + +It uses epoll or libevent for highly scalable non-blocking I/O. Coroutines ensure that the developer uses a blocking style of programming that is similar to threading, but provide the benefits of non-blocking I/O. The event dispatch is implicit, which means you can easily use Eventlet from the Python interpreter, or as a small part of a larger application. + +It's easy to get started using Eventlet, and easy to convert existing +applications to use it. Start off by looking at the `examples`_, +`common design patterns`_, and the list of `basic API primitives`_. + +.. _examples: https://eventlet.readthedocs.io/en/latest/examples.html +.. _common design patterns: https://eventlet.readthedocs.io/en/latest/design_patterns.html +.. _basic API primitives: https://eventlet.readthedocs.io/en/latest/basic_usage.html + + +Getting Eventlet +================ + +The easiest way to get Eventlet is to use pip:: + + pip install -U eventlet + +To install latest development version once:: + + pip install -U https://github.com/eventlet/eventlet/archive/master.zip + + +Building the Docs Locally +========================= + +To build a complete set of HTML documentation:: + + tox -e docs + +The built html files can be found in doc/build/html afterward. + +Supported Python versions +========================= + +Python 3.8-3.13 are currently supported. diff --git a/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/RECORD b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/RECORD new file mode 100644 index 0000000..edeadb0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/RECORD @@ -0,0 +1,199 @@ +eventlet-0.40.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +eventlet-0.40.3.dist-info/METADATA,sha256=z8Yz4D_aLs7c0vFY7lMiBNWjRZ6QAhG6Q7vdOJHUa0c,5404 +eventlet-0.40.3.dist-info/RECORD,, +eventlet-0.40.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +eventlet-0.40.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87 +eventlet-0.40.3.dist-info/licenses/AUTHORS,sha256=v3feCO6nQpkhl0T4SMRigKJJk8w4LEOmWY71Je9gvhg,6267 +eventlet-0.40.3.dist-info/licenses/LICENSE,sha256=vOygSX96gUdRFr_0E4cz-yAGC2sitnHmV7YVioYGVuI,1254 +eventlet/__init__.py,sha256=MxZDsg2iH6ceyMSGifwXnLT9QHhhbHJi8Tr2ukxcPMc,2668 +eventlet/__pycache__/__init__.cpython-311.pyc,, +eventlet/__pycache__/_version.cpython-311.pyc,, +eventlet/__pycache__/asyncio.cpython-311.pyc,, +eventlet/__pycache__/backdoor.cpython-311.pyc,, +eventlet/__pycache__/convenience.cpython-311.pyc,, +eventlet/__pycache__/corolocal.cpython-311.pyc,, +eventlet/__pycache__/coros.cpython-311.pyc,, +eventlet/__pycache__/dagpool.cpython-311.pyc,, +eventlet/__pycache__/db_pool.cpython-311.pyc,, +eventlet/__pycache__/debug.cpython-311.pyc,, +eventlet/__pycache__/event.cpython-311.pyc,, +eventlet/__pycache__/greenpool.cpython-311.pyc,, +eventlet/__pycache__/greenthread.cpython-311.pyc,, +eventlet/__pycache__/lock.cpython-311.pyc,, +eventlet/__pycache__/patcher.cpython-311.pyc,, +eventlet/__pycache__/pools.cpython-311.pyc,, +eventlet/__pycache__/queue.cpython-311.pyc,, +eventlet/__pycache__/semaphore.cpython-311.pyc,, +eventlet/__pycache__/timeout.cpython-311.pyc,, +eventlet/__pycache__/tpool.cpython-311.pyc,, +eventlet/__pycache__/websocket.cpython-311.pyc,, +eventlet/__pycache__/wsgi.cpython-311.pyc,, +eventlet/_version.py,sha256=w48bRxDhf2PRIf2hVYQ83HF60QXVWaxag-pofg_I6WE,706 +eventlet/asyncio.py,sha256=X-eMizlIBJ7z1nQqkZVPQynBgBiYmeIQxqnShe-P4v0,1723 +eventlet/backdoor.py,sha256=Rl0YQMNGRh6Htn5RlcrvgNDyGZ_X8B4rRsqkne0kOFA,4043 +eventlet/convenience.py,sha256=dF_ntllWDM09s-y2hoo987ijEVUK80AEqkto-3FN5aY,7158 +eventlet/corolocal.py,sha256=FbStAfAkBixRiFJaJb8On3RbaXEVx0f25BsFL9AyKTg,1733 +eventlet/coros.py,sha256=0wub8j1GlVX19driNRwzsDeBhINWXHqOBKb0PEqVJ2s,2030 +eventlet/dagpool.py,sha256=SHtsmYkvvo1hVcEejfJYVVQ7mS8lSnR5opAHBwOCX_U,26180 +eventlet/db_pool.py,sha256=fucoCrf2cqGc-uL5IYrQJYAznj61DDWatmY2OMNCMbY,15514 +eventlet/debug.py,sha256=ZKY0yy2GQF6eFVcaXo0bWog1TJ_UcomCgoEjzO3dy-c,8393 +eventlet/event.py,sha256=SmfhkdHozkG2TkKrob-r3lPfSYKKgnmYtRMJxjXW35M,7496 +eventlet/green/BaseHTTPServer.py,sha256=kAwWSvHTKqm-Y-5dtGAVXY84kMFSfeBcT7ucwKx8MXg,302 +eventlet/green/CGIHTTPServer.py,sha256=g6IUEF1p4q7kpAaKVhsqo0L1f8acl_X-_gX0ynP4Y50,466 +eventlet/green/MySQLdb.py,sha256=sTanY41h3vqnh6tum-wYucOgkFqHJBIthtsOjA_qbLw,1196 +eventlet/green/OpenSSL/SSL.py,sha256=1hFS2eB30LGZDgbLTrCMH7htDbRreBVLtXgNmiJ50tk,4534 +eventlet/green/OpenSSL/__init__.py,sha256=h3kX23byJXMSl1rEhBf1oPo5D9LLqmXjWngXmaHpON0,246 +eventlet/green/OpenSSL/__pycache__/SSL.cpython-311.pyc,, +eventlet/green/OpenSSL/__pycache__/__init__.cpython-311.pyc,, +eventlet/green/OpenSSL/__pycache__/crypto.cpython-311.pyc,, +eventlet/green/OpenSSL/__pycache__/tsafe.cpython-311.pyc,, +eventlet/green/OpenSSL/__pycache__/version.cpython-311.pyc,, +eventlet/green/OpenSSL/crypto.py,sha256=dcnjSGP6K274eAxalZEOttUZ1djAStBnbRH-wGBSJu4,29 +eventlet/green/OpenSSL/tsafe.py,sha256=DuY1rHdT2R0tiJkD13ECj-IU7_v-zQKjhTsK6CG8UEM,28 +eventlet/green/OpenSSL/version.py,sha256=3Ti2k01zP3lM6r0YuLbLS_QReJBEHaTJt5k0dNdXtI4,49 +eventlet/green/Queue.py,sha256=CsIn5cEJtbge-kTLw2xSFzjNkq5udUY1vyVrf5AS9WM,789 +eventlet/green/SimpleHTTPServer.py,sha256=O8A3gRYO48q3jVxIslyyaLYgjvTJqiHtGAJZPydEZRs,232 +eventlet/green/SocketServer.py,sha256=w1Ge_Zhp-Dm2hG2t06GscLgd7gXZyCg55e45kba28yY,323 +eventlet/green/__init__.py,sha256=upnrKC57DQQBDNvpxXf_IhDapQ6NtEt2hgxIs1pZDao,84 +eventlet/green/__pycache__/BaseHTTPServer.cpython-311.pyc,, +eventlet/green/__pycache__/CGIHTTPServer.cpython-311.pyc,, +eventlet/green/__pycache__/MySQLdb.cpython-311.pyc,, +eventlet/green/__pycache__/Queue.cpython-311.pyc,, +eventlet/green/__pycache__/SimpleHTTPServer.cpython-311.pyc,, +eventlet/green/__pycache__/SocketServer.cpython-311.pyc,, +eventlet/green/__pycache__/__init__.cpython-311.pyc,, +eventlet/green/__pycache__/_socket_nodns.cpython-311.pyc,, +eventlet/green/__pycache__/asynchat.cpython-311.pyc,, +eventlet/green/__pycache__/asyncore.cpython-311.pyc,, +eventlet/green/__pycache__/builtin.cpython-311.pyc,, +eventlet/green/__pycache__/ftplib.cpython-311.pyc,, +eventlet/green/__pycache__/httplib.cpython-311.pyc,, +eventlet/green/__pycache__/os.cpython-311.pyc,, +eventlet/green/__pycache__/profile.cpython-311.pyc,, +eventlet/green/__pycache__/select.cpython-311.pyc,, +eventlet/green/__pycache__/selectors.cpython-311.pyc,, +eventlet/green/__pycache__/socket.cpython-311.pyc,, +eventlet/green/__pycache__/ssl.cpython-311.pyc,, +eventlet/green/__pycache__/subprocess.cpython-311.pyc,, +eventlet/green/__pycache__/thread.cpython-311.pyc,, +eventlet/green/__pycache__/threading.cpython-311.pyc,, +eventlet/green/__pycache__/time.cpython-311.pyc,, +eventlet/green/__pycache__/urllib2.cpython-311.pyc,, +eventlet/green/__pycache__/zmq.cpython-311.pyc,, +eventlet/green/_socket_nodns.py,sha256=Oc-5EYs3AST-0HH4Hpi24t2tLp_CrzRX3jDFHN_rPH4,795 +eventlet/green/asynchat.py,sha256=IxG7yS4UNv2z8xkbtlnyGrAGpaXIjYGpyxtXjmcgWrI,291 +eventlet/green/asyncore.py,sha256=aKGWNcWSKUJhWS5fC5i9SrcIWyPuHQxaQKks8yw_m50,345 +eventlet/green/builtin.py,sha256=eLrJZgTDwhIFN-Sor8jWjm-D-OLqQ69GDqvjIZHK9As,1013 +eventlet/green/ftplib.py,sha256=d23VMcAPqw7ZILheDJmueM8qOlWHnq0WFjjSgWouRdA,307 +eventlet/green/http/__init__.py,sha256=X0DA5WqAuctSblh2tBviwW5ob1vnVcW6uiT9INsH_1o,8738 +eventlet/green/http/__pycache__/__init__.cpython-311.pyc,, +eventlet/green/http/__pycache__/client.cpython-311.pyc,, +eventlet/green/http/__pycache__/cookiejar.cpython-311.pyc,, +eventlet/green/http/__pycache__/cookies.cpython-311.pyc,, +eventlet/green/http/__pycache__/server.cpython-311.pyc,, +eventlet/green/http/client.py,sha256=9aa0jGR4KUd6B-sUrtOKEDQ4tYM8Xr9YBwxkT68obss,59137 +eventlet/green/http/cookiejar.py,sha256=3fB9nFaHOriwgAhASKotuoksOxbKnfGo3N69wiQYzjo,79435 +eventlet/green/http/cookies.py,sha256=2XAyogPiyysieelxS7KjOzXQHAXezQmAiEKesh3L4MQ,24189 +eventlet/green/http/server.py,sha256=jHfdMtiF8_WQHahLCEspBHpm2cCm7wmBKbBRByn7vQs,46596 +eventlet/green/httplib.py,sha256=T9_QVRLiJVBQlVexvnYvf4PXYAZdjclwLzqoX1fbJ38,390 +eventlet/green/os.py,sha256=UAlVogW-ZO2ha5ftCs199RtSz3MV3pgTQB_R_VVTb9Q,3774 +eventlet/green/profile.py,sha256=D7ij2c7MVLqXbjXoZtqTkVFP7bMspmNEr34XYYw8tfM,9514 +eventlet/green/select.py,sha256=wgmGGfUQYg8X8Ov6ayRAikt6v3o-uPL-wPARk-ihqhE,2743 +eventlet/green/selectors.py,sha256=C_aeln-t0FsMG2WosmkIBhGst0KfKglcaJG8U50pxQM,948 +eventlet/green/socket.py,sha256=np5_HqSjA4_y_kYKdSFyHQN0vjzLW_qi_oLFH8bB0T0,1918 +eventlet/green/ssl.py,sha256=BU4mKN5sBnyp6gb7AhCgTYWtl2N9as1ANt9PFFfx94M,19417 +eventlet/green/subprocess.py,sha256=Y7UX-_D-L6LIzM6NNwKyBn1sgcfsOUr8e0Lka26367s,5575 +eventlet/green/thread.py,sha256=QvqpW7sVlCTm4clZoSO4Q_leqLK-sUYkWZ1V7WWmy8U,4964 +eventlet/green/threading.py,sha256=m0XSuVJU-jOcGeJAAqsujznCLVprXr6EbzTlrPv3p6Q,3903 +eventlet/green/time.py,sha256=1W7BKbGrfTI1v2-pDnBvzBn01tbQ8zwyqz458BFrjt0,240 +eventlet/green/urllib/__init__.py,sha256=hjlirvvvuVKMnugnX9PVW6-9zy6E_q85hqvXunAjpqU,164 +eventlet/green/urllib/__pycache__/__init__.cpython-311.pyc,, +eventlet/green/urllib/__pycache__/error.cpython-311.pyc,, +eventlet/green/urllib/__pycache__/parse.cpython-311.pyc,, +eventlet/green/urllib/__pycache__/request.cpython-311.pyc,, +eventlet/green/urllib/__pycache__/response.cpython-311.pyc,, +eventlet/green/urllib/error.py,sha256=xlpHJIa8U4QTFolAa3NEy5gEVj_nM3oF2bB-FvdhCQg,157 +eventlet/green/urllib/parse.py,sha256=uJ1R4rbgqlQgINjKm_-oTxveLvCR9anu7U0i7aRS87k,83 +eventlet/green/urllib/request.py,sha256=Z4VR5X776Po-DlOqcA46-T51avbtepo20SMQGkac--M,1611 +eventlet/green/urllib/response.py,sha256=ytsGn0pXE94tlZh75hl9X1cFGagjGNBWm6k_PRXOBmM,86 +eventlet/green/urllib2.py,sha256=Su3dEhDc8VsKK9PqhIXwgFVOOHVI37TTXU_beqzvg44,488 +eventlet/green/zmq.py,sha256=xd88Ao4zuq-a6g8RV6_GLOPgZGC9w6OtQeKJ7AhgY4k,18018 +eventlet/greenio/__init__.py,sha256=d6_QQqaEAPBpE2vNjU-rHWXmZ94emYuwKjclF3XT2gs,88 +eventlet/greenio/__pycache__/__init__.cpython-311.pyc,, +eventlet/greenio/__pycache__/base.cpython-311.pyc,, +eventlet/greenio/__pycache__/py3.cpython-311.pyc,, +eventlet/greenio/base.py,sha256=jPUtjDABa9yMhSkBIHpBHLu3fYOxBHIMXxvBvPJlLGo,17122 +eventlet/greenio/py3.py,sha256=-Gm-n6AYCyKDwDhWm64cZMtthM1pzEXcWa3ZfjD_aiI,6791 +eventlet/greenpool.py,sha256=-Cyi27l0ds8YRXwedUiFsfoyRl8uulHkrek-bukRdL8,9734 +eventlet/greenthread.py,sha256=x7NK66otGsSDYWMRMSFMI6blMUTZlNbRUUdH1k8UtbI,13370 +eventlet/hubs/__init__.py,sha256=i9S4ki1aiTJqLxAkDg16xjWX951Rwk2G8SfoQbzLWEs,6013 +eventlet/hubs/__pycache__/__init__.cpython-311.pyc,, +eventlet/hubs/__pycache__/asyncio.cpython-311.pyc,, +eventlet/hubs/__pycache__/epolls.cpython-311.pyc,, +eventlet/hubs/__pycache__/hub.cpython-311.pyc,, +eventlet/hubs/__pycache__/kqueue.cpython-311.pyc,, +eventlet/hubs/__pycache__/poll.cpython-311.pyc,, +eventlet/hubs/__pycache__/pyevent.cpython-311.pyc,, +eventlet/hubs/__pycache__/selects.cpython-311.pyc,, +eventlet/hubs/__pycache__/timer.cpython-311.pyc,, +eventlet/hubs/asyncio.py,sha256=8PsWA55Pj8U855fYD1N1JBLxfOxvyy2OBkFuUaKYAiA,5961 +eventlet/hubs/epolls.py,sha256=IkY-yX7shRxVO5LQ8Ysv5FiH6g-XW0XKhtyvorrRFlg,1018 +eventlet/hubs/hub.py,sha256=JcfZBQfFuo0dk_PpqKDcIf_9K_Kzzf0vGBxCqOTIy_E,17604 +eventlet/hubs/kqueue.py,sha256=-jOGtjNHcJAeIDfZYzFB8ZZeIfYAf4tssHuK_A9Qt1o,3420 +eventlet/hubs/poll.py,sha256=qn0qQdvmvKMCQRHr6arvyI027TDVRM1G_kjhx5biLrk,3895 +eventlet/hubs/pyevent.py,sha256=PtImWgRlaH9NmglMcAw5BnqYrTnVoy-4VjfRHUSdvyo,156 +eventlet/hubs/selects.py,sha256=13R8ueir1ga8nFapuqnjFEpRbsRcda4V1CpNhUwtKt8,1984 +eventlet/hubs/timer.py,sha256=Uvo5gxjptEyCtTaeb_X7SpaIvATqLb6ehWX_33Y242c,3185 +eventlet/lock.py,sha256=GGrKyItc5a0ANCrB2eS7243g_BiHVAS_ufjy1eWE7Es,1229 +eventlet/patcher.py,sha256=cMuVlnYIOEPuIe_npl7q3P1H-Bfh7iwuvEaJaOr1VB4,26890 +eventlet/pools.py,sha256=3JPSudnQP3M-FD0ihc17zS7NPaQZ4cXwwmf1qDDJKuU,6244 +eventlet/queue.py,sha256=iA9lG-oiMePgYYNnspubTBu4xbaoyaSSWYa_cL5Q7-Q,18394 +eventlet/semaphore.py,sha256=F6aIp2d5uuvYJPTmRAwt9U8sfDIjlT259MtDWKp4SHY,12163 +eventlet/support/__init__.py,sha256=Gkqs5h-VXQZc73NIkBXps45uuFdRLrXvme4DNwY3Y3k,1764 +eventlet/support/__pycache__/__init__.cpython-311.pyc,, +eventlet/support/__pycache__/greendns.cpython-311.pyc,, +eventlet/support/__pycache__/greenlets.cpython-311.pyc,, +eventlet/support/__pycache__/psycopg2_patcher.cpython-311.pyc,, +eventlet/support/__pycache__/pylib.cpython-311.pyc,, +eventlet/support/__pycache__/stacklesspypys.cpython-311.pyc,, +eventlet/support/__pycache__/stacklesss.cpython-311.pyc,, +eventlet/support/greendns.py,sha256=X1w1INSzAudrdPIVg19MARRmc5o1pkzM4C-gQgWU0Z8,35489 +eventlet/support/greenlets.py,sha256=1mxaAJJlZYSBgoWM1EL9IvbtMHTo61KokzScSby1Qy8,133 +eventlet/support/psycopg2_patcher.py,sha256=Rzm9GYS7PmrNpKAw04lqJV7KPcxLovnaCUI8CXE328A,2272 +eventlet/support/pylib.py,sha256=EvZ1JZEX3wqWtzfga5HeVL-sLLb805_f_ywX2k5BDHo,274 +eventlet/support/stacklesspypys.py,sha256=6BwZcnsCtb1m4wdK6GygoiPvYV03v7P7YlBxPIE6Zns,275 +eventlet/support/stacklesss.py,sha256=hxen8xtqrHS-bMPP3ThiqRCutNeNlQHjzmW-1DzE0JM,1851 +eventlet/timeout.py,sha256=mFW8oEj3wxSFQQhXOejdtOyWYaqFgRK82ccfz5fojQ4,6644 +eventlet/tpool.py,sha256=2EXw7sNqfRo7aBPOUxhOV3bHWgmbIoIQyyb9SGAQLQY,10573 +eventlet/websocket.py,sha256=b_D4u3NQ04XVLSp_rZ-jApFY0THBsG03z8rcDsKTYjk,34535 +eventlet/wsgi.py,sha256=CjQjjSQsfk95NonoQwu2ykezALX5umDUYEmZXkP3hXM,42360 +eventlet/zipkin/README.rst,sha256=xmt_Mmbtl3apFwYzgrWOtaQdM46AdT1MV11N-dwrLsA,3866 +eventlet/zipkin/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +eventlet/zipkin/__pycache__/__init__.cpython-311.pyc,, +eventlet/zipkin/__pycache__/api.cpython-311.pyc,, +eventlet/zipkin/__pycache__/client.cpython-311.pyc,, +eventlet/zipkin/__pycache__/greenthread.cpython-311.pyc,, +eventlet/zipkin/__pycache__/http.cpython-311.pyc,, +eventlet/zipkin/__pycache__/log.cpython-311.pyc,, +eventlet/zipkin/__pycache__/patcher.cpython-311.pyc,, +eventlet/zipkin/__pycache__/wsgi.cpython-311.pyc,, +eventlet/zipkin/_thrift/README.rst,sha256=5bZ4doepGQlXdemHzPfvcobc5C0Mwa0lxzuAn_Dm3LY,233 +eventlet/zipkin/_thrift/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +eventlet/zipkin/_thrift/__pycache__/__init__.cpython-311.pyc,, +eventlet/zipkin/_thrift/zipkinCore.thrift,sha256=zbV8L5vQUXNngVbI1eXR2gAgenmWRyPGzf7QEb2_wNU,2121 +eventlet/zipkin/_thrift/zipkinCore/__init__.py,sha256=YFcZTT8Cm-6Y4oTiCaqq0DT1lw2W09WqoEc5_pTAwW0,34 +eventlet/zipkin/_thrift/zipkinCore/__pycache__/__init__.cpython-311.pyc,, +eventlet/zipkin/_thrift/zipkinCore/__pycache__/constants.cpython-311.pyc,, +eventlet/zipkin/_thrift/zipkinCore/__pycache__/ttypes.cpython-311.pyc,, +eventlet/zipkin/_thrift/zipkinCore/constants.py,sha256=cbgWT_mN04BRZbyzjr1LzT40xvotzFyz-vbYp8Q_klo,275 +eventlet/zipkin/_thrift/zipkinCore/ttypes.py,sha256=94RG3YtkmpeMmJ-EvKiwnYUtovYlfjrRVnh6sI27cJ0,13497 +eventlet/zipkin/api.py,sha256=K9RdTr68ifYVQ28IhQZSOTC82E2y7P_cjIw28ykWJg8,5467 +eventlet/zipkin/client.py,sha256=hT6meeP8pM5WDWi-zDt8xXDLwjpfM1vaJ2DRju8MA9I,1691 +eventlet/zipkin/example/ex1.png,sha256=tMloQ9gWouUjGhHWTBzzuPQ308JdUtrVFd2ClXHRIBg,53179 +eventlet/zipkin/example/ex2.png,sha256=AAIYZig2qVz6RVTj8nlIKju0fYT3DfP-F28LLwYIxwI,40482 +eventlet/zipkin/example/ex3.png,sha256=xc4J1WOjKCeAYr4gRSFFggJbHMEk-_C9ukmAKXTEfuk,73175 +eventlet/zipkin/greenthread.py,sha256=ify1VnsJmrFneAwfPl6QE8kgHIPJE5fAE9Ks9wQzeVI,843 +eventlet/zipkin/http.py,sha256=qe_QMKI9GAV7HDZ6z1k_8rgEbICpCsqa80EdjQLG5Uk,666 +eventlet/zipkin/log.py,sha256=jElBHT8H3_vs9T3r8Q-JG30xyajQ7u6wNGWmmMPQ4AA,337 +eventlet/zipkin/patcher.py,sha256=t1g5tXcbuEvNix3ICtZyuIWaJKQtUHJ5ZUqsi14j9Dc,1388 +eventlet/zipkin/wsgi.py,sha256=IT3d_j2DKRTALf5BRr7IPqWbFwfxH0VUIQ_EyItWfp4,2268 diff --git a/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/REQUESTED b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/WHEEL b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/WHEEL new file mode 100644 index 0000000..12228d4 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: hatchling 1.27.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/AUTHORS b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/AUTHORS new file mode 100644 index 0000000..a976907 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/AUTHORS @@ -0,0 +1,189 @@ +Maintainer (i.e., Who To Hassle If You Find Bugs) +------------------------------------------------- + +The current maintainer(s) are volunteers with unrelated jobs. +We can only pay sporadic attention to responding to your issue and pull request submissions. +Your patience is greatly appreciated! + +Active maintainers +~~~~~~~~~~~~~~~~~~ + +* Itamar Turner-Trauring https://github.com/itamarst +* Tim Burke https://github.com/tipabu +* Hervé Beraud https://github.com/4383 + +Less active maintainers +~~~~~~~~~~~~~~~~~~~~~~~ + +* Sergey Shepelev https://github.com/temoto +* Jakub Stasiak https://github.com/jstasiak +* Nat Goodspeed https://github.com/nat-goodspeed + +Original Authors +---------------- +* Bob Ippolito +* Donovan Preston + +Contributors +------------ +* AG Projects +* Chris AtLee +* R\. Tyler Ballance +* Denis Bilenko +* Mike Barton +* Patrick Carlisle +* Ben Ford +* Andrew Godwin +* Brantley Harris +* Gregory Holt +* Joe Malicki +* Chet Murthy +* Eugene Oden +* radix +* Scott Robinson +* Tavis Rudd +* Sergey Shepelev +* Chuck Thier +* Nick V +* Daniele Varrazzo +* Ryan Williams +* Geoff Salmon +* Edward George +* Floris Bruynooghe +* Paul Oppenheim +* Jakub Stasiak +* Aldona Majorek +* Victor Sergeyev +* David Szotten +* Victor Stinner +* Samuel Merritt +* Eric Urban +* Miguel Grinberg +* Tuomo Kriikkula + +Linden Lab Contributors +----------------------- +* John Beisley +* Tess Chu +* Nat Goodspeed +* Dave Kaprielian +* Kartic Krishnamurthy +* Bryan O'Sullivan +* Kent Quirk +* Ryan Williams + +Thanks To +--------- +* AdamKG, giving the hint that invalid argument errors were introduced post-0.9.0 +* Luke Tucker, bug report regarding wsgi + webob +* Taso Du Val, reproing an exception squelching bug, saving children's lives ;-) +* Luci Stanescu, for reporting twisted hub bug +* Marcus Cavanaugh, for test case code that has been incredibly useful in tracking down bugs +* Brian Brunswick, for many helpful questions and suggestions on the mailing list +* Cesar Alaniz, for uncovering bugs of great import +* the grugq, for contributing patches, suggestions, and use cases +* Ralf Schmitt, for wsgi/webob incompatibility bug report and suggested fix +* Benoit Chesneau, bug report on green.os and patch to fix it +* Slant, better iterator implementation in tpool +* Ambroff, nice pygtk hub example +* Michael Carter, websocket patch to improve location handling +* Marcin Bachry, nice repro of a bug and good diagnosis leading to the fix +* David Ziegler, reporting issue #53 +* Favo Yang, twisted hub patch +* Schmir, patch that fixes readline method with chunked encoding in wsgi.py, advice on patcher +* Slide, for open-sourcing gogreen +* Holger Krekel, websocket example small fix +* mikepk, debugging MySQLdb/tpool issues +* Malcolm Cleaton, patch for Event exception handling +* Alexey Borzenkov, for finding and fixing issues with Windows error detection (#66, #69), reducing dependencies in zeromq hub (#71) +* Anonymous, finding and fixing error in websocket chat example (#70) +* Edward George, finding and fixing an issue in the [e]poll hubs (#74), and in convenience (#86) +* Ruijun Luo, figuring out incorrect openssl import for wrap_ssl (#73) +* rfk, patch to get green zmq to respect noblock flag. +* Soren Hansen, finding and fixing issue in subprocess (#77) +* Stefano Rivera, making tests pass in absence of postgres (#78) +* Joshua Kwan, fixing busy-wait in eventlet.green.ssl. +* Nick Vatamaniuc, Windows SO_REUSEADDR patch (#83) +* Clay Gerrard, wsgi handle socket closed by client (#95) +* Eric Windisch, zmq getsockopt(EVENTS) wake correct threads (pull request 22) +* Raymond Lu, fixing busy-wait in eventlet.green.ssl.socket.sendall() +* Thomas Grainger, webcrawler example small fix, "requests" library import bug report, Travis integration +* Peter Portante, save syscalls in socket.dup(), environ[REMOTE_PORT] in wsgi +* Peter Skirko, fixing socket.settimeout(0) bug +* Derk Tegeler, Pre-cache proxied GreenSocket methods (Bitbucket #136) +* David Malcolm, optional "timeout" argument to the subprocess module (Bitbucket #89) +* David Goetz, wsgi: Allow minimum_chunk_size to be overriden on a per request basis +* Dmitry Orlov, websocket: accept Upgrade: websocket (lowercase) +* Zhang Hua, profile: accumulate results between runs (Bitbucket #162) +* Astrum Kuo, python3 compatibility fixes; greenthread.unlink() method +* Davanum Srinivas, Python3 compatibility fixes +* Dmitriy Kruglyak, PyPy 2.3 compatibility fix +* Jan Grant, Michael Kerrin, second simultaneous read (GH-94) +* Simon Jagoe, Python3 octal literal fix +* Tushar Gohad, wsgi: Support optional headers w/ "100 Continue" responses +* raylu, fixing operator precedence bug in eventlet.wsgi +* Christoph Gysin, PEP 8 conformance +* Andrey Gubarev +* Corey Wright +* Deva +* Johannes Erdfelt +* Kevin +* QthCN +* Steven Hardy +* Stuart McLaren +* Tomaz Muraus +* ChangBo Guo(gcb), fixing typos in the documentation (GH-194) +* Marc Abramowitz, fixing the README so it renders correctly on PyPI (GH-183) +* Shaun Stanworth, equal chance to acquire semaphore from different greenthreads (GH-136) +* Lior Neudorfer, Make sure SSL retries are done using the exact same data buffer +* Sean Dague, wsgi: Provide python logging compatibility +* Tim Simmons, Use _socket_nodns and select in dnspython support +* Antonio Cuni, fix fd double close on PyPy +* Seyeong Kim +* Ihar Hrachyshka +* Janusz Harkot +* Fukuchi Daisuke +* Ramakrishnan G +* ashutosh-mishra +* Azhar Hussain +* Josh VanderLinden +* Levente Polyak +* Phus Lu +* Collin Stocks, fixing eventlet.green.urllib2.urlopen() so it accepts cafile, capath, or cadefault arguments +* Alexis Lee +* Steven Erenst +* Piët Delport +* Alex Villacís Lasso +* Yashwardhan Singh +* Tim Burke +* Ondřej Nový +* Jarrod Johnson +* Whitney Young +* Matthew D. Pagel +* Matt Yule-Bennett +* Artur Stawiarski +* Tal Wrii +* Roman Podoliaka +* Gevorg Davoian +* Ondřej Kobližek +* Yuichi Bando +* Feng +* Aayush Kasurde +* Linbing +* Geoffrey Thomas +* Costas Christofi, adding permessage-deflate weboscket extension support +* Peter Kovary, adding permessage-deflate weboscket extension support +* Konstantin Enchant +* James Page +* Stefan Nica +* Haikel Guemar +* Miguel Grinberg +* Chris Kerr +* Anthony Sottile +* Quan Tian +* orishoshan +* Matt Bennett +* Ralf Haferkamp +* Jake Tesler +* Aayush Kasurde +* Psycho Mantys, patch for exception handling on ReferenceError diff --git a/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/LICENSE b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/LICENSE new file mode 100644 index 0000000..2ddd0d9 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/LICENSE @@ -0,0 +1,23 @@ +Unless otherwise noted, the files in Eventlet are under the following MIT license: + +Copyright (c) 2005-2006, Bob Ippolito +Copyright (c) 2007-2010, Linden Research, Inc. +Copyright (c) 2008-2010, Eventlet Contributors (see AUTHORS) + +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/netdeploy/lib/python3.11/site-packages/eventlet/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/__init__.py new file mode 100644 index 0000000..01773c5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/__init__.py @@ -0,0 +1,88 @@ +import os +import sys +import warnings + + +from eventlet import convenience +from eventlet import event +from eventlet import greenpool +from eventlet import greenthread +from eventlet import patcher +from eventlet import queue +from eventlet import semaphore +from eventlet import support +from eventlet import timeout +# NOTE(hberaud): Versions are now managed by hatch and control version. +# hatch has a build hook which generates the version file, however, +# if the project is installed in editable mode then the _version.py file +# will not be updated unless the package is reinstalled (or locally rebuilt). +# For further details, please read: +# https://github.com/ofek/hatch-vcs#build-hook +# https://github.com/maresb/hatch-vcs-footgun-example +try: + from eventlet._version import __version__ +except ImportError: + __version__ = "0.0.0" +import greenlet + +# Force monotonic library search as early as possible. +# Helpful when CPython < 3.5 on Linux blocked in `os.waitpid(-1)` before first use of hub. +# Example: gunicorn +# https://github.com/eventlet/eventlet/issues/401#issuecomment-327500352 +try: + import monotonic + del monotonic +except ImportError: + pass + +connect = convenience.connect +listen = convenience.listen +serve = convenience.serve +StopServe = convenience.StopServe +wrap_ssl = convenience.wrap_ssl + +Event = event.Event + +GreenPool = greenpool.GreenPool +GreenPile = greenpool.GreenPile + +sleep = greenthread.sleep +spawn = greenthread.spawn +spawn_n = greenthread.spawn_n +spawn_after = greenthread.spawn_after +kill = greenthread.kill + +import_patched = patcher.import_patched +monkey_patch = patcher.monkey_patch + +Queue = queue.Queue + +Semaphore = semaphore.Semaphore +CappedSemaphore = semaphore.CappedSemaphore +BoundedSemaphore = semaphore.BoundedSemaphore + +Timeout = timeout.Timeout +with_timeout = timeout.with_timeout +wrap_is_timeout = timeout.wrap_is_timeout +is_timeout = timeout.is_timeout + +getcurrent = greenlet.greenlet.getcurrent + +# deprecated +TimeoutError, exc_after, call_after_global = ( + support.wrap_deprecated(old, new)(fun) for old, new, fun in ( + ('TimeoutError', 'Timeout', Timeout), + ('exc_after', 'greenthread.exc_after', greenthread.exc_after), + ('call_after_global', 'greenthread.call_after_global', greenthread.call_after_global), + )) + + +if hasattr(os, "register_at_fork"): + def _warn_on_fork(): + import warnings + warnings.warn( + "Using fork() is a bad idea, and there is no guarantee eventlet will work." + + " See https://eventlet.readthedocs.io/en/latest/fork.html for more details.", + DeprecationWarning + ) + os.register_at_fork(before=_warn_on_fork) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/_version.py b/netdeploy/lib/python3.11/site-packages/eventlet/_version.py new file mode 100644 index 0000000..204a16a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/_version.py @@ -0,0 +1,34 @@ +# file generated by setuptools-scm +# don't change, don't track in version control + +__all__ = [ + "__version__", + "__version_tuple__", + "version", + "version_tuple", + "__commit_id__", + "commit_id", +] + +TYPE_CHECKING = False +if TYPE_CHECKING: + from typing import Tuple + from typing import Union + + VERSION_TUPLE = Tuple[Union[int, str], ...] + COMMIT_ID = Union[str, None] +else: + VERSION_TUPLE = object + COMMIT_ID = object + +version: str +__version__: str +__version_tuple__: VERSION_TUPLE +version_tuple: VERSION_TUPLE +commit_id: COMMIT_ID +__commit_id__: COMMIT_ID + +__version__ = version = '0.40.3' +__version_tuple__ = version_tuple = (0, 40, 3) + +__commit_id__ = commit_id = None diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/asyncio.py b/netdeploy/lib/python3.11/site-packages/eventlet/asyncio.py new file mode 100644 index 0000000..b9eca92 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/asyncio.py @@ -0,0 +1,57 @@ +""" +Asyncio compatibility functions. +""" +import asyncio + +from greenlet import GreenletExit + +from .greenthread import spawn, getcurrent +from .event import Event +from .hubs import get_hub +from .hubs.asyncio import Hub as AsyncioHub + +__all__ = ["spawn_for_awaitable"] + + +def spawn_for_awaitable(coroutine): + """ + Take a coroutine or some other object that can be awaited + (``asyncio.Future``, ``asyncio.Task``), and turn it into a ``GreenThread``. + + Known limitations: + + * The coroutine/future/etc. don't run in their own + greenlet/``GreenThread``. + * As a result, things like ``eventlet.Lock`` + won't work correctly inside ``async`` functions, thread ids aren't + meaningful, and so on. + """ + if not isinstance(get_hub(), AsyncioHub): + raise RuntimeError( + "This API only works with eventlet's asyncio hub. " + + "To use it, set an EVENTLET_HUB=asyncio environment variable." + ) + + def _run(): + # Convert the coroutine/Future/Task we're wrapping into a Future. + future = asyncio.ensure_future(coroutine, loop=asyncio.get_running_loop()) + + # Ensure killing the GreenThread cancels the Future: + def _got_result(gthread): + try: + gthread.wait() + except GreenletExit: + future.cancel() + + getcurrent().link(_got_result) + + # Wait until the Future has a result. + has_result = Event() + future.add_done_callback(lambda _: has_result.send(True)) + has_result.wait() + # Return the result of the Future (or raise an exception if it had an + # exception). + return future.result() + + # Start a GreenThread: + return spawn(_run) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/backdoor.py b/netdeploy/lib/python3.11/site-packages/eventlet/backdoor.py new file mode 100644 index 0000000..3f3887f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/backdoor.py @@ -0,0 +1,140 @@ +from code import InteractiveConsole +import errno +import socket +import sys + +import eventlet +from eventlet import hubs +from eventlet.support import greenlets, get_errno + +try: + sys.ps1 +except AttributeError: + sys.ps1 = '>>> ' +try: + sys.ps2 +except AttributeError: + sys.ps2 = '... ' + + +class FileProxy: + def __init__(self, f): + self.f = f + + def isatty(self): + return True + + def flush(self): + pass + + def write(self, data, *a, **kw): + try: + self.f.write(data, *a, **kw) + self.f.flush() + except OSError as e: + if get_errno(e) != errno.EPIPE: + raise + + def readline(self, *a): + return self.f.readline(*a).replace('\r\n', '\n') + + def __getattr__(self, attr): + return getattr(self.f, attr) + + +# @@tavis: the `locals` args below mask the built-in function. Should +# be renamed. +class SocketConsole(greenlets.greenlet): + def __init__(self, desc, hostport, locals): + self.hostport = hostport + self.locals = locals + # mangle the socket + self.desc = FileProxy(desc) + greenlets.greenlet.__init__(self) + + def run(self): + try: + console = InteractiveConsole(self.locals) + console.interact() + finally: + self.switch_out() + self.finalize() + + def switch(self, *args, **kw): + self.saved = sys.stdin, sys.stderr, sys.stdout + sys.stdin = sys.stdout = sys.stderr = self.desc + greenlets.greenlet.switch(self, *args, **kw) + + def switch_out(self): + sys.stdin, sys.stderr, sys.stdout = self.saved + + def finalize(self): + # restore the state of the socket + self.desc = None + if len(self.hostport) >= 2: + host = self.hostport[0] + port = self.hostport[1] + print("backdoor closed to %s:%s" % (host, port,)) + else: + print('backdoor closed') + + +def backdoor_server(sock, locals=None): + """ Blocking function that runs a backdoor server on the socket *sock*, + accepting connections and running backdoor consoles for each client that + connects. + + The *locals* argument is a dictionary that will be included in the locals() + of the interpreters. It can be convenient to stick important application + variables in here. + """ + listening_on = sock.getsockname() + if sock.family == socket.AF_INET: + # Expand result to IP + port + listening_on = '%s:%s' % listening_on + elif sock.family == socket.AF_INET6: + ip, port, _, _ = listening_on + listening_on = '%s:%s' % (ip, port,) + # No action needed if sock.family == socket.AF_UNIX + + print("backdoor server listening on %s" % (listening_on,)) + try: + while True: + socketpair = None + try: + socketpair = sock.accept() + backdoor(socketpair, locals) + except OSError as e: + # Broken pipe means it was shutdown + if get_errno(e) != errno.EPIPE: + raise + finally: + if socketpair: + socketpair[0].close() + finally: + sock.close() + + +def backdoor(conn_info, locals=None): + """Sets up an interactive console on a socket with a single connected + client. This does not block the caller, as it spawns a new greenlet to + handle the console. This is meant to be called from within an accept loop + (such as backdoor_server). + """ + conn, addr = conn_info + if conn.family == socket.AF_INET: + host, port = addr + print("backdoor to %s:%s" % (host, port)) + elif conn.family == socket.AF_INET6: + host, port, _, _ = addr + print("backdoor to %s:%s" % (host, port)) + else: + print('backdoor opened') + fl = conn.makefile("rw") + console = SocketConsole(fl, addr, locals) + hub = hubs.get_hub() + hub.schedule_call_global(0, console.switch) + + +if __name__ == '__main__': + backdoor_server(eventlet.listen(('127.0.0.1', 9000)), {}) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/convenience.py b/netdeploy/lib/python3.11/site-packages/eventlet/convenience.py new file mode 100644 index 0000000..4d286aa --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/convenience.py @@ -0,0 +1,190 @@ +import sys +import warnings + +from eventlet import greenpool +from eventlet import greenthread +from eventlet import support +from eventlet.green import socket +from eventlet.support import greenlets as greenlet + + +def connect(addr, family=socket.AF_INET, bind=None): + """Convenience function for opening client sockets. + + :param addr: Address of the server to connect to. For TCP sockets, this is a (host, port) tuple. + :param family: Socket family, optional. See :mod:`socket` documentation for available families. + :param bind: Local address to bind to, optional. + :return: The connected green socket object. + """ + sock = socket.socket(family, socket.SOCK_STREAM) + if bind is not None: + sock.bind(bind) + sock.connect(addr) + return sock + + +class ReuseRandomPortWarning(Warning): + pass + + +class ReusePortUnavailableWarning(Warning): + pass + + +def listen(addr, family=socket.AF_INET, backlog=50, reuse_addr=True, reuse_port=None): + """Convenience function for opening server sockets. This + socket can be used in :func:`~eventlet.serve` or a custom ``accept()`` loop. + + Sets SO_REUSEADDR on the socket to save on annoyance. + + :param addr: Address to listen on. For TCP sockets, this is a (host, port) tuple. + :param family: Socket family, optional. See :mod:`socket` documentation for available families. + :param backlog: + + The maximum number of queued connections. Should be at least 1; the maximum + value is system-dependent. + + :return: The listening green socket object. + """ + sock = socket.socket(family, socket.SOCK_STREAM) + if reuse_addr and sys.platform[:3] != 'win': + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if family in (socket.AF_INET, socket.AF_INET6) and addr[1] == 0: + if reuse_port: + warnings.warn( + '''listen on random port (0) with SO_REUSEPORT is dangerous. + Double check your intent. + Example problem: https://github.com/eventlet/eventlet/issues/411''', + ReuseRandomPortWarning, stacklevel=3) + elif reuse_port is None: + reuse_port = True + if reuse_port and hasattr(socket, 'SO_REUSEPORT'): + # NOTE(zhengwei): linux kernel >= 3.9 + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + # OSError is enough on Python 3+ + except OSError as ex: + if support.get_errno(ex) in (22, 92): + # A famous platform defines unsupported socket option. + # https://github.com/eventlet/eventlet/issues/380 + # https://github.com/eventlet/eventlet/issues/418 + warnings.warn( + '''socket.SO_REUSEPORT is defined but not supported. + On Windows: known bug, wontfix. + On other systems: please comment in the issue linked below. + More information: https://github.com/eventlet/eventlet/issues/380''', + ReusePortUnavailableWarning, stacklevel=3) + + sock.bind(addr) + sock.listen(backlog) + return sock + + +class StopServe(Exception): + """Exception class used for quitting :func:`~eventlet.serve` gracefully.""" + pass + + +def _stop_checker(t, server_gt, conn): + try: + try: + t.wait() + finally: + conn.close() + except greenlet.GreenletExit: + pass + except Exception: + greenthread.kill(server_gt, *sys.exc_info()) + + +def serve(sock, handle, concurrency=1000): + """Runs a server on the supplied socket. Calls the function *handle* in a + separate greenthread for every incoming client connection. *handle* takes + two arguments: the client socket object, and the client address:: + + def myhandle(client_sock, client_addr): + print("client connected", client_addr) + + eventlet.serve(eventlet.listen(('127.0.0.1', 9999)), myhandle) + + Returning from *handle* closes the client socket. + + :func:`serve` blocks the calling greenthread; it won't return until + the server completes. If you desire an immediate return, + spawn a new greenthread for :func:`serve`. + + Any uncaught exceptions raised in *handle* are raised as exceptions + from :func:`serve`, terminating the server, so be sure to be aware of the + exceptions your application can raise. The return value of *handle* is + ignored. + + Raise a :class:`~eventlet.StopServe` exception to gracefully terminate the + server -- that's the only way to get the server() function to return rather + than raise. + + The value in *concurrency* controls the maximum number of + greenthreads that will be open at any time handling requests. When + the server hits the concurrency limit, it stops accepting new + connections until the existing ones complete. + """ + pool = greenpool.GreenPool(concurrency) + server_gt = greenthread.getcurrent() + + while True: + try: + conn, addr = sock.accept() + gt = pool.spawn(handle, conn, addr) + gt.link(_stop_checker, server_gt, conn) + conn, addr, gt = None, None, None + except StopServe: + return + + +def wrap_ssl(sock, *a, **kw): + """Convenience function for converting a regular socket into an + SSL socket. Has the same interface as :func:`ssl.wrap_socket`, + but can also use PyOpenSSL. Though, note that it ignores the + `cert_reqs`, `ssl_version`, `ca_certs`, `do_handshake_on_connect`, + and `suppress_ragged_eofs` arguments when using PyOpenSSL. + + The preferred idiom is to call wrap_ssl directly on the creation + method, e.g., ``wrap_ssl(connect(addr))`` or + ``wrap_ssl(listen(addr), server_side=True)``. This way there is + no "naked" socket sitting around to accidentally corrupt the SSL + session. + + :return Green SSL object. + """ + return wrap_ssl_impl(sock, *a, **kw) + + +try: + from eventlet.green import ssl + wrap_ssl_impl = ssl.wrap_socket +except ImportError: + # trying PyOpenSSL + try: + from eventlet.green.OpenSSL import SSL + except ImportError: + def wrap_ssl_impl(*a, **kw): + raise ImportError( + "To use SSL with Eventlet, you must install PyOpenSSL or use Python 2.7 or later.") + else: + def wrap_ssl_impl(sock, keyfile=None, certfile=None, server_side=False, + cert_reqs=None, ssl_version=None, ca_certs=None, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, ciphers=None): + # theoretically the ssl_version could be respected in this line + context = SSL.Context(SSL.SSLv23_METHOD) + if certfile is not None: + context.use_certificate_file(certfile) + if keyfile is not None: + context.use_privatekey_file(keyfile) + context.set_verify(SSL.VERIFY_NONE, lambda *x: True) + + connection = SSL.Connection(context, sock) + if server_side: + connection.set_accept_state() + else: + connection.set_connect_state() + return connection diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/corolocal.py b/netdeploy/lib/python3.11/site-packages/eventlet/corolocal.py new file mode 100644 index 0000000..73b10b6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/corolocal.py @@ -0,0 +1,53 @@ +import weakref + +from eventlet import greenthread + +__all__ = ['get_ident', 'local'] + + +def get_ident(): + """ Returns ``id()`` of current greenlet. Useful for debugging.""" + return id(greenthread.getcurrent()) + + +# the entire purpose of this class is to store off the constructor +# arguments in a local variable without calling __init__ directly +class _localbase: + __slots__ = '_local__args', '_local__greens' + + def __new__(cls, *args, **kw): + self = object.__new__(cls) + object.__setattr__(self, '_local__args', (args, kw)) + object.__setattr__(self, '_local__greens', weakref.WeakKeyDictionary()) + if (args or kw) and (cls.__init__ is object.__init__): + raise TypeError("Initialization arguments are not supported") + return self + + +def _patch(thrl): + greens = object.__getattribute__(thrl, '_local__greens') + # until we can store the localdict on greenlets themselves, + # we store it in _local__greens on the local object + cur = greenthread.getcurrent() + if cur not in greens: + # must be the first time we've seen this greenlet, call __init__ + greens[cur] = {} + cls = type(thrl) + if cls.__init__ is not object.__init__: + args, kw = object.__getattribute__(thrl, '_local__args') + thrl.__init__(*args, **kw) + object.__setattr__(thrl, '__dict__', greens[cur]) + + +class local(_localbase): + def __getattribute__(self, attr): + _patch(self) + return object.__getattribute__(self, attr) + + def __setattr__(self, attr, value): + _patch(self) + return object.__setattr__(self, attr, value) + + def __delattr__(self, attr): + _patch(self) + return object.__delattr__(self, attr) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/coros.py b/netdeploy/lib/python3.11/site-packages/eventlet/coros.py new file mode 100644 index 0000000..fbd7e99 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/coros.py @@ -0,0 +1,59 @@ +from eventlet import event as _event + + +class metaphore: + """This is sort of an inverse semaphore: a counter that starts at 0 and + waits only if nonzero. It's used to implement a "wait for all" scenario. + + >>> from eventlet import coros, spawn_n + >>> count = coros.metaphore() + >>> count.wait() + >>> def decrementer(count, id): + ... print("{0} decrementing".format(id)) + ... count.dec() + ... + >>> _ = spawn_n(decrementer, count, 'A') + >>> _ = spawn_n(decrementer, count, 'B') + >>> count.inc(2) + >>> count.wait() + A decrementing + B decrementing + """ + + def __init__(self): + self.counter = 0 + self.event = _event.Event() + # send() right away, else we'd wait on the default 0 count! + self.event.send() + + def inc(self, by=1): + """Increment our counter. If this transitions the counter from zero to + nonzero, make any subsequent :meth:`wait` call wait. + """ + assert by > 0 + self.counter += by + if self.counter == by: + # If we just incremented self.counter by 'by', and the new count + # equals 'by', then the old value of self.counter was 0. + # Transitioning from 0 to a nonzero value means wait() must + # actually wait. + self.event.reset() + + def dec(self, by=1): + """Decrement our counter. If this transitions the counter from nonzero + to zero, a current or subsequent wait() call need no longer wait. + """ + assert by > 0 + self.counter -= by + if self.counter <= 0: + # Don't leave self.counter < 0, that will screw things up in + # future calls. + self.counter = 0 + # Transitioning from nonzero to 0 means wait() need no longer wait. + self.event.send() + + def wait(self): + """Suspend the caller only if our count is nonzero. In that case, + resume the caller once the count decrements to zero again. + """ + self.event.wait() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/dagpool.py b/netdeploy/lib/python3.11/site-packages/eventlet/dagpool.py new file mode 100644 index 0000000..47d13a8 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/dagpool.py @@ -0,0 +1,601 @@ +# @file dagpool.py +# @author Nat Goodspeed +# @date 2016-08-08 +# @brief Provide DAGPool class + +from eventlet.event import Event +from eventlet import greenthread +import collections + + +# value distinguished from any other Python value including None +_MISSING = object() + + +class Collision(Exception): + """ + DAGPool raises Collision when you try to launch two greenthreads with the + same key, or post() a result for a key corresponding to a greenthread, or + post() twice for the same key. As with KeyError, str(collision) names the + key in question. + """ + pass + + +class PropagateError(Exception): + """ + When a DAGPool greenthread terminates with an exception instead of + returning a result, attempting to retrieve its value raises + PropagateError. + + Attributes: + + key + the key of the greenthread which raised the exception + + exc + the exception object raised by the greenthread + """ + def __init__(self, key, exc): + # initialize base class with a reasonable string message + msg = "PropagateError({}): {}: {}" \ + .format(key, exc.__class__.__name__, exc) + super().__init__(msg) + self.msg = msg + # Unless we set args, this is unpickleable: + # https://bugs.python.org/issue1692335 + self.args = (key, exc) + self.key = key + self.exc = exc + + def __str__(self): + return self.msg + + +class DAGPool: + """ + A DAGPool is a pool that constrains greenthreads, not by max concurrency, + but by data dependencies. + + This is a way to implement general DAG dependencies. A simple dependency + tree (flowing in either direction) can straightforwardly be implemented + using recursion and (e.g.) + :meth:`GreenThread.imap() `. + What gets complicated is when a given node depends on several other nodes + as well as contributing to several other nodes. + + With DAGPool, you concurrently launch all applicable greenthreads; each + will proceed as soon as it has all required inputs. The DAG is implicit in + which items are required by each greenthread. + + Each greenthread is launched in a DAGPool with a key: any value that can + serve as a Python dict key. The caller also specifies an iterable of other + keys on which this greenthread depends. This iterable may be empty. + + The greenthread callable must accept (key, results), where: + + key + is its own key + + results + is an iterable of (key, value) pairs. + + A newly-launched DAGPool greenthread is entered immediately, and can + perform any necessary setup work. At some point it will iterate over the + (key, value) pairs from the passed 'results' iterable. Doing so blocks the + greenthread until a value is available for each of the keys specified in + its initial dependencies iterable. These (key, value) pairs are delivered + in chronological order, *not* the order in which they are initially + specified: each value will be delivered as soon as it becomes available. + + The value returned by a DAGPool greenthread becomes the value for its + key, which unblocks any other greenthreads waiting on that key. + + If a DAGPool greenthread terminates with an exception instead of returning + a value, attempting to retrieve the value raises :class:`PropagateError`, + which binds the key of the original greenthread and the original + exception. Unless the greenthread attempting to retrieve the value handles + PropagateError, that exception will in turn be wrapped in a PropagateError + of its own, and so forth. The code that ultimately handles PropagateError + can follow the chain of PropagateError.exc attributes to discover the flow + of that exception through the DAG of greenthreads. + + External greenthreads may also interact with a DAGPool. See :meth:`wait_each`, + :meth:`waitall`, :meth:`post`. + + It is not recommended to constrain external DAGPool producer greenthreads + in a :class:`GreenPool `: it may be hard to + provably avoid deadlock. + + .. automethod:: __init__ + .. automethod:: __getitem__ + """ + + _Coro = collections.namedtuple("_Coro", ("greenthread", "pending")) + + def __init__(self, preload={}): + """ + DAGPool can be prepopulated with an initial dict or iterable of (key, + value) pairs. These (key, value) pairs are of course immediately + available for any greenthread that depends on any of those keys. + """ + try: + # If a dict is passed, copy it. Don't risk a subsequent + # modification to passed dict affecting our internal state. + iteritems = preload.items() + except AttributeError: + # Not a dict, just an iterable of (key, value) pairs + iteritems = preload + + # Load the initial dict + self.values = dict(iteritems) + + # track greenthreads + self.coros = {} + + # The key to blocking greenthreads is the Event. + self.event = Event() + + def waitall(self): + """ + waitall() blocks the calling greenthread until there is a value for + every DAGPool greenthread launched by :meth:`spawn`. It returns a dict + containing all :class:`preload data `, all data from + :meth:`post` and all values returned by spawned greenthreads. + + See also :meth:`wait`. + """ + # waitall() is an alias for compatibility with GreenPool + return self.wait() + + def wait(self, keys=_MISSING): + """ + *keys* is an optional iterable of keys. If you omit the argument, it + waits for all the keys from :class:`preload data `, from + :meth:`post` calls and from :meth:`spawn` calls: in other words, all + the keys of which this DAGPool is aware. + + wait() blocks the calling greenthread until all of the relevant keys + have values. wait() returns a dict whose keys are the relevant keys, + and whose values come from the *preload* data, from values returned by + DAGPool greenthreads or from :meth:`post` calls. + + If a DAGPool greenthread terminates with an exception, wait() will + raise :class:`PropagateError` wrapping that exception. If more than + one greenthread terminates with an exception, it is indeterminate + which one wait() will raise. + + If an external greenthread posts a :class:`PropagateError` instance, + wait() will raise that PropagateError. If more than one greenthread + posts PropagateError, it is indeterminate which one wait() will raise. + + See also :meth:`wait_each_success`, :meth:`wait_each_exception`. + """ + # This is mostly redundant with wait_each() functionality. + return dict(self.wait_each(keys)) + + def wait_each(self, keys=_MISSING): + """ + *keys* is an optional iterable of keys. If you omit the argument, it + waits for all the keys from :class:`preload data `, from + :meth:`post` calls and from :meth:`spawn` calls: in other words, all + the keys of which this DAGPool is aware. + + wait_each() is a generator producing (key, value) pairs as a value + becomes available for each requested key. wait_each() blocks the + calling greenthread until the next value becomes available. If the + DAGPool was prepopulated with values for any of the relevant keys, of + course those can be delivered immediately without waiting. + + Delivery order is intentionally decoupled from the initial sequence of + keys: each value is delivered as soon as it becomes available. If + multiple keys are available at the same time, wait_each() delivers + each of the ready ones in arbitrary order before blocking again. + + The DAGPool does not distinguish between a value returned by one of + its own greenthreads and one provided by a :meth:`post` call or *preload* data. + + The wait_each() generator terminates (raises StopIteration) when all + specified keys have been delivered. Thus, typical usage might be: + + :: + + for key, value in dagpool.wait_each(keys): + # process this ready key and value + # continue processing now that we've gotten values for all keys + + By implication, if you pass wait_each() an empty iterable of keys, it + returns immediately without yielding anything. + + If the value to be delivered is a :class:`PropagateError` exception object, the + generator raises that PropagateError instead of yielding it. + + See also :meth:`wait_each_success`, :meth:`wait_each_exception`. + """ + # Build a local set() and then call _wait_each(). + return self._wait_each(self._get_keyset_for_wait_each(keys)) + + def wait_each_success(self, keys=_MISSING): + """ + wait_each_success() filters results so that only success values are + yielded. In other words, unlike :meth:`wait_each`, wait_each_success() + will not raise :class:`PropagateError`. Not every provided (or + defaulted) key will necessarily be represented, though naturally the + generator will not finish until all have completed. + + In all other respects, wait_each_success() behaves like :meth:`wait_each`. + """ + for key, value in self._wait_each_raw(self._get_keyset_for_wait_each(keys)): + if not isinstance(value, PropagateError): + yield key, value + + def wait_each_exception(self, keys=_MISSING): + """ + wait_each_exception() filters results so that only exceptions are + yielded. Not every provided (or defaulted) key will necessarily be + represented, though naturally the generator will not finish until + all have completed. + + Unlike other DAGPool methods, wait_each_exception() simply yields + :class:`PropagateError` instances as values rather than raising them. + + In all other respects, wait_each_exception() behaves like :meth:`wait_each`. + """ + for key, value in self._wait_each_raw(self._get_keyset_for_wait_each(keys)): + if isinstance(value, PropagateError): + yield key, value + + def _get_keyset_for_wait_each(self, keys): + """ + wait_each(), wait_each_success() and wait_each_exception() promise + that if you pass an iterable of keys, the method will wait for results + from those keys -- but if you omit the keys argument, the method will + wait for results from all known keys. This helper implements that + distinction, returning a set() of the relevant keys. + """ + if keys is not _MISSING: + return set(keys) + else: + # keys arg omitted -- use all the keys we know about + return set(self.coros.keys()) | set(self.values.keys()) + + def _wait_each(self, pending): + """ + When _wait_each() encounters a value of PropagateError, it raises it. + + In all other respects, _wait_each() behaves like _wait_each_raw(). + """ + for key, value in self._wait_each_raw(pending): + yield key, self._value_or_raise(value) + + @staticmethod + def _value_or_raise(value): + # Most methods attempting to deliver PropagateError should raise that + # instead of simply returning it. + if isinstance(value, PropagateError): + raise value + return value + + def _wait_each_raw(self, pending): + """ + pending is a set() of keys for which we intend to wait. THIS SET WILL + BE DESTRUCTIVELY MODIFIED: as each key acquires a value, that key will + be removed from the passed 'pending' set. + + _wait_each_raw() does not treat a PropagateError instance specially: + it will be yielded to the caller like any other value. + + In all other respects, _wait_each_raw() behaves like wait_each(). + """ + while True: + # Before even waiting, show caller any (key, value) pairs that + # are already available. Copy 'pending' because we want to be able + # to remove items from the original set while iterating. + for key in pending.copy(): + value = self.values.get(key, _MISSING) + if value is not _MISSING: + # found one, it's no longer pending + pending.remove(key) + yield (key, value) + + if not pending: + # Once we've yielded all the caller's keys, done. + break + + # There are still more keys pending, so wait. + self.event.wait() + + def spawn(self, key, depends, function, *args, **kwds): + """ + Launch the passed *function(key, results, ...)* as a greenthread, + passing it: + + - the specified *key* + - an iterable of (key, value) pairs + - whatever other positional args or keywords you specify. + + Iterating over the *results* iterable behaves like calling + :meth:`wait_each(depends) `. + + Returning from *function()* behaves like + :meth:`post(key, return_value) `. + + If *function()* terminates with an exception, that exception is wrapped + in :class:`PropagateError` with the greenthread's *key* and (effectively) posted + as the value for that key. Attempting to retrieve that value will + raise that PropagateError. + + Thus, if the greenthread with key 'a' terminates with an exception, + and greenthread 'b' depends on 'a', when greenthread 'b' attempts to + iterate through its *results* argument, it will encounter + PropagateError. So by default, an uncaught exception will propagate + through all the downstream dependencies. + + If you pass :meth:`spawn` a key already passed to spawn() or :meth:`post`, spawn() + raises :class:`Collision`. + """ + if key in self.coros or key in self.values: + raise Collision(key) + + # The order is a bit tricky. First construct the set() of keys. + pending = set(depends) + # It's important that we pass to _wait_each() the same 'pending' set() + # that we store in self.coros for this key. The generator-iterator + # returned by _wait_each() becomes the function's 'results' iterable. + newcoro = greenthread.spawn(self._wrapper, function, key, + self._wait_each(pending), + *args, **kwds) + # Also capture the same (!) set in the new _Coro object for this key. + # We must be able to observe ready keys being removed from the set. + self.coros[key] = self._Coro(newcoro, pending) + + def _wrapper(self, function, key, results, *args, **kwds): + """ + This wrapper runs the top-level function in a DAGPool greenthread, + posting its return value (or PropagateError) to the DAGPool. + """ + try: + # call our passed function + result = function(key, results, *args, **kwds) + except Exception as err: + # Wrap any exception it may raise in a PropagateError. + result = PropagateError(key, err) + finally: + # function() has returned (or terminated with an exception). We no + # longer need to track this greenthread in self.coros. Remove it + # first so post() won't complain about a running greenthread. + del self.coros[key] + + try: + # as advertised, try to post() our return value + self.post(key, result) + except Collision: + # if we've already post()ed a result, oh well + pass + + # also, in case anyone cares... + return result + + def spawn_many(self, depends, function, *args, **kwds): + """ + spawn_many() accepts a single *function* whose parameters are the same + as for :meth:`spawn`. + + The difference is that spawn_many() accepts a dependency dict + *depends*. A new greenthread is spawned for each key in the dict. That + dict key's value should be an iterable of other keys on which this + greenthread depends. + + If the *depends* dict contains any key already passed to :meth:`spawn` + or :meth:`post`, spawn_many() raises :class:`Collision`. It is + indeterminate how many of the other keys in *depends* will have + successfully spawned greenthreads. + """ + # Iterate over 'depends' items, relying on self.spawn() not to + # context-switch so no one can modify 'depends' along the way. + for key, deps in depends.items(): + self.spawn(key, deps, function, *args, **kwds) + + def kill(self, key): + """ + Kill the greenthread that was spawned with the specified *key*. + + If no such greenthread was spawned, raise KeyError. + """ + # let KeyError, if any, propagate + self.coros[key].greenthread.kill() + # once killed, remove it + del self.coros[key] + + def post(self, key, value, replace=False): + """ + post(key, value) stores the passed *value* for the passed *key*. It + then causes each greenthread blocked on its results iterable, or on + :meth:`wait_each(keys) `, to check for new values. + A waiting greenthread might not literally resume on every single + post() of a relevant key, but the first post() of a relevant key + ensures that it will resume eventually, and when it does it will catch + up with all relevant post() calls. + + Calling post(key, value) when there is a running greenthread with that + same *key* raises :class:`Collision`. If you must post(key, value) instead of + letting the greenthread run to completion, you must first call + :meth:`kill(key) `. + + The DAGPool implicitly post()s the return value from each of its + greenthreads. But a greenthread may explicitly post() a value for its + own key, which will cause its return value to be discarded. + + Calling post(key, value, replace=False) (the default *replace*) when a + value for that key has already been posted, by any means, raises + :class:`Collision`. + + Calling post(key, value, replace=True) when a value for that key has + already been posted, by any means, replaces the previously-stored + value. However, that may make it complicated to reason about the + behavior of greenthreads waiting on that key. + + After a post(key, value1) followed by post(key, value2, replace=True), + it is unspecified which pending :meth:`wait_each([key...]) ` + calls (or greenthreads iterating over *results* involving that key) + will observe *value1* versus *value2*. It is guaranteed that + subsequent wait_each([key...]) calls (or greenthreads spawned after + that point) will observe *value2*. + + A successful call to + post(key, :class:`PropagateError(key, ExceptionSubclass) `) + ensures that any subsequent attempt to retrieve that key's value will + raise that PropagateError instance. + """ + # First, check if we're trying to post() to a key with a running + # greenthread. + # A DAGPool greenthread is explicitly permitted to post() to its + # OWN key. + coro = self.coros.get(key, _MISSING) + if coro is not _MISSING and coro.greenthread is not greenthread.getcurrent(): + # oh oh, trying to post a value for running greenthread from + # some other greenthread + raise Collision(key) + + # Here, either we're posting a value for a key with no greenthread or + # we're posting from that greenthread itself. + + # Has somebody already post()ed a value for this key? + # Unless replace == True, this is a problem. + if key in self.values and not replace: + raise Collision(key) + + # Either we've never before posted a value for this key, or we're + # posting with replace == True. + + # update our database + self.values[key] = value + # and wake up pending waiters + self.event.send() + # The comment in Event.reset() says: "it's better to create a new + # event rather than reset an old one". Okay, fine. We do want to be + # able to support new waiters, so create a new Event. + self.event = Event() + + def __getitem__(self, key): + """ + __getitem__(key) (aka dagpool[key]) blocks until *key* has a value, + then delivers that value. + """ + # This is a degenerate case of wait_each(). Construct a tuple + # containing only this 'key'. wait_each() will yield exactly one (key, + # value) pair. Return just its value. + for _, value in self.wait_each((key,)): + return value + + def get(self, key, default=None): + """ + get() returns the value for *key*. If *key* does not yet have a value, + get() returns *default*. + """ + return self._value_or_raise(self.values.get(key, default)) + + def keys(self): + """ + Return a snapshot tuple of keys for which we currently have values. + """ + # Explicitly return a copy rather than an iterator: don't assume our + # caller will finish iterating before new values are posted. + return tuple(self.values.keys()) + + def items(self): + """ + Return a snapshot tuple of currently-available (key, value) pairs. + """ + # Don't assume our caller will finish iterating before new values are + # posted. + return tuple((key, self._value_or_raise(value)) + for key, value in self.values.items()) + + def running(self): + """ + Return number of running DAGPool greenthreads. This includes + greenthreads blocked while iterating through their *results* iterable, + that is, greenthreads waiting on values from other keys. + """ + return len(self.coros) + + def running_keys(self): + """ + Return keys for running DAGPool greenthreads. This includes + greenthreads blocked while iterating through their *results* iterable, + that is, greenthreads waiting on values from other keys. + """ + # return snapshot; don't assume caller will finish iterating before we + # next modify self.coros + return tuple(self.coros.keys()) + + def waiting(self): + """ + Return number of waiting DAGPool greenthreads, that is, greenthreads + still waiting on values from other keys. This explicitly does *not* + include external greenthreads waiting on :meth:`wait`, + :meth:`waitall`, :meth:`wait_each`. + """ + # n.b. if Event would provide a count of its waiters, we could say + # something about external greenthreads as well. + # The logic to determine this count is exactly the same as the general + # waiting_for() call. + return len(self.waiting_for()) + + # Use _MISSING instead of None as the default 'key' param so we can permit + # None as a supported key. + def waiting_for(self, key=_MISSING): + """ + waiting_for(key) returns a set() of the keys for which the DAGPool + greenthread spawned with that *key* is still waiting. If you pass a + *key* for which no greenthread was spawned, waiting_for() raises + KeyError. + + waiting_for() without argument returns a dict. Its keys are the keys + of DAGPool greenthreads still waiting on one or more values. In the + returned dict, the value of each such key is the set of other keys for + which that greenthread is still waiting. + + This method allows diagnosing a "hung" DAGPool. If certain + greenthreads are making no progress, it's possible that they are + waiting on keys for which there is no greenthread and no :meth:`post` data. + """ + # We may have greenthreads whose 'pending' entry indicates they're + # waiting on some keys even though values have now been posted for + # some or all of those keys, because those greenthreads have not yet + # regained control since values were posted. So make a point of + # excluding values that are now available. + available = set(self.values.keys()) + + if key is not _MISSING: + # waiting_for(key) is semantically different than waiting_for(). + # It's just that they both seem to want the same method name. + coro = self.coros.get(key, _MISSING) + if coro is _MISSING: + # Hmm, no running greenthread with this key. But was there + # EVER a greenthread with this key? If not, let KeyError + # propagate. + self.values[key] + # Oh good, there's a value for this key. Either the + # greenthread finished, or somebody posted a value. Just say + # the greenthread isn't waiting for anything. + return set() + else: + # coro is the _Coro for the running greenthread with the + # specified key. + return coro.pending - available + + # This is a waiting_for() call, i.e. a general query rather than for a + # specific key. + + # Start by iterating over (key, coro) pairs in self.coros. Generate + # (key, pending) pairs in which 'pending' is the set of keys on which + # the greenthread believes it's waiting, minus the set of keys that + # are now available. Filter out any pair in which 'pending' is empty, + # that is, that greenthread will be unblocked next time it resumes. + # Make a dict from those pairs. + return {key: pending + for key, pending in ((key, (coro.pending - available)) + for key, coro in self.coros.items()) + if pending} diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/db_pool.py b/netdeploy/lib/python3.11/site-packages/eventlet/db_pool.py new file mode 100644 index 0000000..7deb993 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/db_pool.py @@ -0,0 +1,460 @@ +from collections import deque +from contextlib import contextmanager +import sys +import time + +from eventlet.pools import Pool +from eventlet import timeout +from eventlet import hubs +from eventlet.hubs.timer import Timer +from eventlet.greenthread import GreenThread + + +_MISSING = object() + + +class ConnectTimeout(Exception): + pass + + +def cleanup_rollback(conn): + conn.rollback() + + +class BaseConnectionPool(Pool): + def __init__(self, db_module, + min_size=0, max_size=4, + max_idle=10, max_age=30, + connect_timeout=5, + cleanup=cleanup_rollback, + *args, **kwargs): + """ + Constructs a pool with at least *min_size* connections and at most + *max_size* connections. Uses *db_module* to construct new connections. + + The *max_idle* parameter determines how long pooled connections can + remain idle, in seconds. After *max_idle* seconds have elapsed + without the connection being used, the pool closes the connection. + + *max_age* is how long any particular connection is allowed to live. + Connections that have been open for longer than *max_age* seconds are + closed, regardless of idle time. If *max_age* is 0, all connections are + closed on return to the pool, reducing it to a concurrency limiter. + + *connect_timeout* is the duration in seconds that the pool will wait + before timing out on connect() to the database. If triggered, the + timeout will raise a ConnectTimeout from get(). + + The remainder of the arguments are used as parameters to the + *db_module*'s connection constructor. + """ + assert(db_module) + self._db_module = db_module + self._args = args + self._kwargs = kwargs + self.max_idle = max_idle + self.max_age = max_age + self.connect_timeout = connect_timeout + self._expiration_timer = None + self.cleanup = cleanup + super().__init__(min_size=min_size, max_size=max_size, order_as_stack=True) + + def _schedule_expiration(self): + """Sets up a timer that will call _expire_old_connections when the + oldest connection currently in the free pool is ready to expire. This + is the earliest possible time that a connection could expire, thus, the + timer will be running as infrequently as possible without missing a + possible expiration. + + If this function is called when a timer is already scheduled, it does + nothing. + + If max_age or max_idle is 0, _schedule_expiration likewise does nothing. + """ + if self.max_age == 0 or self.max_idle == 0: + # expiration is unnecessary because all connections will be expired + # on put + return + + if (self._expiration_timer is not None + and not getattr(self._expiration_timer, 'called', False)): + # the next timer is already scheduled + return + + try: + now = time.time() + self._expire_old_connections(now) + # the last item in the list, because of the stack ordering, + # is going to be the most-idle + idle_delay = (self.free_items[-1][0] - now) + self.max_idle + oldest = min([t[1] for t in self.free_items]) + age_delay = (oldest - now) + self.max_age + + next_delay = min(idle_delay, age_delay) + except (IndexError, ValueError): + # no free items, unschedule ourselves + self._expiration_timer = None + return + + if next_delay > 0: + # set up a continuous self-calling loop + self._expiration_timer = Timer(next_delay, GreenThread(hubs.get_hub().greenlet).switch, + self._schedule_expiration, [], {}) + self._expiration_timer.schedule() + + def _expire_old_connections(self, now): + """Iterates through the open connections contained in the pool, closing + ones that have remained idle for longer than max_idle seconds, or have + been in existence for longer than max_age seconds. + + *now* is the current time, as returned by time.time(). + """ + original_count = len(self.free_items) + expired = [ + conn + for last_used, created_at, conn in self.free_items + if self._is_expired(now, last_used, created_at)] + + new_free = [ + (last_used, created_at, conn) + for last_used, created_at, conn in self.free_items + if not self._is_expired(now, last_used, created_at)] + self.free_items.clear() + self.free_items.extend(new_free) + + # adjust the current size counter to account for expired + # connections + self.current_size -= original_count - len(self.free_items) + + for conn in expired: + self._safe_close(conn, quiet=True) + + def _is_expired(self, now, last_used, created_at): + """Returns true and closes the connection if it's expired. + """ + if (self.max_idle <= 0 or self.max_age <= 0 + or now - last_used > self.max_idle + or now - created_at > self.max_age): + return True + return False + + def _unwrap_connection(self, conn): + """If the connection was wrapped by a subclass of + BaseConnectionWrapper and is still functional (as determined + by the __nonzero__, or __bool__ in python3, method), returns + the unwrapped connection. If anything goes wrong with this + process, returns None. + """ + base = None + try: + if conn: + base = conn._base + conn._destroy() + else: + base = None + except AttributeError: + pass + return base + + def _safe_close(self, conn, quiet=False): + """Closes the (already unwrapped) connection, squelching any + exceptions. + """ + try: + conn.close() + except AttributeError: + pass # conn is None, or junk + except Exception: + if not quiet: + print("Connection.close raised: %s" % (sys.exc_info()[1])) + + def get(self): + conn = super().get() + + # None is a flag value that means that put got called with + # something it couldn't use + if conn is None: + try: + conn = self.create() + except Exception: + # unconditionally increase the free pool because + # even if there are waiters, doing a full put + # would incur a greenlib switch and thus lose the + # exception stack + self.current_size -= 1 + raise + + # if the call to get() draws from the free pool, it will come + # back as a tuple + if isinstance(conn, tuple): + _last_used, created_at, conn = conn + else: + created_at = time.time() + + # wrap the connection so the consumer can call close() safely + wrapped = PooledConnectionWrapper(conn, self) + # annotating the wrapper so that when it gets put in the pool + # again, we'll know how old it is + wrapped._db_pool_created_at = created_at + return wrapped + + def put(self, conn, cleanup=_MISSING): + created_at = getattr(conn, '_db_pool_created_at', 0) + now = time.time() + conn = self._unwrap_connection(conn) + + if self._is_expired(now, now, created_at): + self._safe_close(conn, quiet=False) + conn = None + elif cleanup is not None: + if cleanup is _MISSING: + cleanup = self.cleanup + # by default, call rollback in case the connection is in the middle + # of a transaction. However, rollback has performance implications + # so optionally do nothing or call something else like ping + try: + if conn: + cleanup(conn) + except Exception as e: + # we don't care what the exception was, we just know the + # connection is dead + print("WARNING: cleanup %s raised: %s" % (cleanup, e)) + conn = None + except: + conn = None + raise + + if conn is not None: + super().put((now, created_at, conn)) + else: + # wake up any waiters with a flag value that indicates + # they need to manufacture a connection + if self.waiting() > 0: + super().put(None) + else: + # no waiters -- just change the size + self.current_size -= 1 + self._schedule_expiration() + + @contextmanager + def item(self, cleanup=_MISSING): + conn = self.get() + try: + yield conn + finally: + self.put(conn, cleanup=cleanup) + + def clear(self): + """Close all connections that this pool still holds a reference to, + and removes all references to them. + """ + if self._expiration_timer: + self._expiration_timer.cancel() + free_items, self.free_items = self.free_items, deque() + for item in free_items: + # Free items created using min_size>0 are not tuples. + conn = item[2] if isinstance(item, tuple) else item + self._safe_close(conn, quiet=True) + self.current_size -= 1 + + def __del__(self): + self.clear() + + +class TpooledConnectionPool(BaseConnectionPool): + """A pool which gives out :class:`~eventlet.tpool.Proxy`-based database + connections. + """ + + def create(self): + now = time.time() + return now, now, self.connect( + self._db_module, self.connect_timeout, *self._args, **self._kwargs) + + @classmethod + def connect(cls, db_module, connect_timeout, *args, **kw): + t = timeout.Timeout(connect_timeout, ConnectTimeout()) + try: + from eventlet import tpool + conn = tpool.execute(db_module.connect, *args, **kw) + return tpool.Proxy(conn, autowrap_names=('cursor',)) + finally: + t.cancel() + + +class RawConnectionPool(BaseConnectionPool): + """A pool which gives out plain database connections. + """ + + def create(self): + now = time.time() + return now, now, self.connect( + self._db_module, self.connect_timeout, *self._args, **self._kwargs) + + @classmethod + def connect(cls, db_module, connect_timeout, *args, **kw): + t = timeout.Timeout(connect_timeout, ConnectTimeout()) + try: + return db_module.connect(*args, **kw) + finally: + t.cancel() + + +# default connection pool is the tpool one +ConnectionPool = TpooledConnectionPool + + +class GenericConnectionWrapper: + def __init__(self, baseconn): + self._base = baseconn + + # Proxy all method calls to self._base + # FIXME: remove repetition; options to consider: + # * for name in (...): + # setattr(class, name, lambda self, *a, **kw: getattr(self._base, name)(*a, **kw)) + # * def __getattr__(self, name): if name in (...): return getattr(self._base, name) + # * other? + def __enter__(self): + return self._base.__enter__() + + def __exit__(self, exc, value, tb): + return self._base.__exit__(exc, value, tb) + + def __repr__(self): + return self._base.__repr__() + + _proxy_funcs = ( + 'affected_rows', + 'autocommit', + 'begin', + 'change_user', + 'character_set_name', + 'close', + 'commit', + 'cursor', + 'dump_debug_info', + 'errno', + 'error', + 'errorhandler', + 'get_server_info', + 'insert_id', + 'literal', + 'ping', + 'query', + 'rollback', + 'select_db', + 'server_capabilities', + 'set_character_set', + 'set_isolation_level', + 'set_server_option', + 'set_sql_mode', + 'show_warnings', + 'shutdown', + 'sqlstate', + 'stat', + 'store_result', + 'string_literal', + 'thread_id', + 'use_result', + 'warning_count', + ) + + +for _proxy_fun in GenericConnectionWrapper._proxy_funcs: + # excess wrapper for early binding (closure by value) + def _wrapper(_proxy_fun=_proxy_fun): + def _proxy_method(self, *args, **kwargs): + return getattr(self._base, _proxy_fun)(*args, **kwargs) + _proxy_method.func_name = _proxy_fun + _proxy_method.__name__ = _proxy_fun + _proxy_method.__qualname__ = 'GenericConnectionWrapper.' + _proxy_fun + return _proxy_method + setattr(GenericConnectionWrapper, _proxy_fun, _wrapper(_proxy_fun)) +del GenericConnectionWrapper._proxy_funcs +del _proxy_fun +del _wrapper + + +class PooledConnectionWrapper(GenericConnectionWrapper): + """A connection wrapper where: + - the close method returns the connection to the pool instead of closing it directly + - ``bool(conn)`` returns a reasonable value + - returns itself to the pool if it gets garbage collected + """ + + def __init__(self, baseconn, pool): + super().__init__(baseconn) + self._pool = pool + + def __nonzero__(self): + return (hasattr(self, '_base') and bool(self._base)) + + __bool__ = __nonzero__ + + def _destroy(self): + self._pool = None + try: + del self._base + except AttributeError: + pass + + def close(self): + """Return the connection to the pool, and remove the + reference to it so that you can't use it again through this + wrapper object. + """ + if self and self._pool: + self._pool.put(self) + self._destroy() + + def __del__(self): + return # this causes some issues if __del__ is called in the + # main coroutine, so for now this is disabled + # self.close() + + +class DatabaseConnector: + """ + This is an object which will maintain a collection of database + connection pools on a per-host basis. + """ + + def __init__(self, module, credentials, + conn_pool=None, *args, **kwargs): + """constructor + *module* + Database module to use. + *credentials* + Mapping of hostname to connect arguments (e.g. username and password) + """ + assert(module) + self._conn_pool_class = conn_pool + if self._conn_pool_class is None: + self._conn_pool_class = ConnectionPool + self._module = module + self._args = args + self._kwargs = kwargs + # this is a map of hostname to username/password + self._credentials = credentials + self._databases = {} + + def credentials_for(self, host): + if host in self._credentials: + return self._credentials[host] + else: + return self._credentials.get('default', None) + + def get(self, host, dbname): + """Returns a ConnectionPool to the target host and schema. + """ + key = (host, dbname) + if key not in self._databases: + new_kwargs = self._kwargs.copy() + new_kwargs['db'] = dbname + new_kwargs['host'] = host + new_kwargs.update(self.credentials_for(host)) + dbpool = self._conn_pool_class( + self._module, *self._args, **new_kwargs) + self._databases[key] = dbpool + + return self._databases[key] diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/debug.py b/netdeploy/lib/python3.11/site-packages/eventlet/debug.py new file mode 100644 index 0000000..f78e2f8 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/debug.py @@ -0,0 +1,222 @@ +"""The debug module contains utilities and functions for better +debugging Eventlet-powered applications.""" + +import os +import sys +import linecache +import re +import inspect + +__all__ = ['spew', 'unspew', 'format_hub_listeners', 'format_hub_timers', + 'hub_listener_stacks', 'hub_exceptions', 'tpool_exceptions', + 'hub_prevent_multiple_readers', 'hub_timer_stacks', + 'hub_blocking_detection', 'format_asyncio_info', + 'format_threads_info'] + +_token_splitter = re.compile(r'\W+') + + +class Spew: + + def __init__(self, trace_names=None, show_values=True): + self.trace_names = trace_names + self.show_values = show_values + + def __call__(self, frame, event, arg): + if event == 'line': + lineno = frame.f_lineno + if '__file__' in frame.f_globals: + filename = frame.f_globals['__file__'] + if (filename.endswith('.pyc') or + filename.endswith('.pyo')): + filename = filename[:-1] + name = frame.f_globals['__name__'] + line = linecache.getline(filename, lineno) + else: + name = '[unknown]' + try: + src, offset = inspect.getsourcelines(frame) + # The first line is line 1 + # But 0 may be returned when executing module-level code + if offset == 0: + offset = 1 + line = src[lineno - offset] + except OSError: + line = 'Unknown code named [%s]. VM instruction #%d' % ( + frame.f_code.co_name, frame.f_lasti) + if self.trace_names is None or name in self.trace_names: + print('%s:%s: %s' % (name, lineno, line.rstrip())) + if not self.show_values: + return self + details = [] + tokens = _token_splitter.split(line) + for tok in tokens: + if tok in frame.f_globals: + details.append('%s=%r' % (tok, frame.f_globals[tok])) + if tok in frame.f_locals: + details.append('%s=%r' % (tok, frame.f_locals[tok])) + if details: + print("\t%s" % ' '.join(details)) + return self + + +def spew(trace_names=None, show_values=False): + """Install a trace hook which writes incredibly detailed logs + about what code is being executed to stdout. + """ + sys.settrace(Spew(trace_names, show_values)) + + +def unspew(): + """Remove the trace hook installed by spew. + """ + sys.settrace(None) + + +def format_hub_listeners(): + """ Returns a formatted string of the current listeners on the current + hub. This can be useful in determining what's going on in the event system, + especially when used in conjunction with :func:`hub_listener_stacks`. + """ + from eventlet import hubs + hub = hubs.get_hub() + result = ['READERS:'] + for l in hub.get_readers(): + result.append(repr(l)) + result.append('WRITERS:') + for l in hub.get_writers(): + result.append(repr(l)) + return os.linesep.join(result) + + +def format_asyncio_info(): + """ Returns a formatted string of the asyncio info. + This can be useful in determining what's going on in the asyncio event + loop system, especially when used in conjunction with the asyncio hub. + """ + import asyncio + tasks = asyncio.all_tasks() + result = ['TASKS:'] + result.append(repr(tasks)) + result.append(f'EVENTLOOP: {asyncio.events.get_event_loop()}') + return os.linesep.join(result) + + +def format_threads_info(): + """ Returns a formatted string of the threads info. + This can be useful in determining what's going on with created threads, + especially when used in conjunction with greenlet + """ + import threading + threads = threading._active + result = ['THREADS:'] + result.append(repr(threads)) + return os.linesep.join(result) + + +def format_hub_timers(): + """ Returns a formatted string of the current timers on the current + hub. This can be useful in determining what's going on in the event system, + especially when used in conjunction with :func:`hub_timer_stacks`. + """ + from eventlet import hubs + hub = hubs.get_hub() + result = ['TIMERS:'] + for l in hub.timers: + result.append(repr(l)) + return os.linesep.join(result) + + +def hub_listener_stacks(state=False): + """Toggles whether or not the hub records the stack when clients register + listeners on file descriptors. This can be useful when trying to figure + out what the hub is up to at any given moment. To inspect the stacks + of the current listeners, call :func:`format_hub_listeners` at critical + junctures in the application logic. + """ + from eventlet import hubs + hubs.get_hub().set_debug_listeners(state) + + +def hub_timer_stacks(state=False): + """Toggles whether or not the hub records the stack when timers are set. + To inspect the stacks of the current timers, call :func:`format_hub_timers` + at critical junctures in the application logic. + """ + from eventlet.hubs import timer + timer._g_debug = state + + +def hub_prevent_multiple_readers(state=True): + """Toggle prevention of multiple greenlets reading from a socket + + When multiple greenlets read from the same socket it is often hard + to predict which greenlet will receive what data. To achieve + resource sharing consider using ``eventlet.pools.Pool`` instead. + + It is important to note that this feature is a debug + convenience. That's not a feature made to be integrated in a production + code in some sort. + + **If you really know what you are doing** you can change the state + to ``False`` to stop the hub from protecting against this mistake. Else + we strongly discourage using this feature, or you should consider using it + really carefully. + + You should be aware that disabling this prevention will be applied to + your entire stack and not only to the context where you may find it useful, + meaning that using this debug feature may have several significant + unexpected side effects on your process, which could cause race conditions + between your sockets and on all your I/O in general. + + You should also notice that this debug convenience is not supported + by the Asyncio hub, which is the official plan for migrating off of + eventlet. Using this feature will lock your migration path. + """ + from eventlet.hubs import hub, get_hub + from eventlet.hubs import asyncio + if not state and isinstance(get_hub(), asyncio.Hub): + raise RuntimeError("Multiple readers are not yet supported by asyncio hub") + hub.g_prevent_multiple_readers = state + + +def hub_exceptions(state=True): + """Toggles whether the hub prints exceptions that are raised from its + timers. This can be useful to see how greenthreads are terminating. + """ + from eventlet import hubs + hubs.get_hub().set_timer_exceptions(state) + from eventlet import greenpool + greenpool.DEBUG = state + + +def tpool_exceptions(state=False): + """Toggles whether tpool itself prints exceptions that are raised from + functions that are executed in it, in addition to raising them like + it normally does.""" + from eventlet import tpool + tpool.QUIET = not state + + +def hub_blocking_detection(state=False, resolution=1): + """Toggles whether Eventlet makes an effort to detect blocking + behavior in an application. + + It does this by telling the kernel to raise a SIGALARM after a + short timeout, and clearing the timeout every time the hub + greenlet is resumed. Therefore, any code that runs for a long + time without yielding to the hub will get interrupted by the + blocking detector (don't use it in production!). + + The *resolution* argument governs how long the SIGALARM timeout + waits in seconds. The implementation uses :func:`signal.setitimer` + and can be specified as a floating-point value. + The shorter the resolution, the greater the chance of false + positives. + """ + from eventlet import hubs + assert resolution > 0 + hubs.get_hub().debug_blocking = state + hubs.get_hub().debug_blocking_resolution = resolution + if not state: + hubs.get_hub().block_detect_post() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/event.py b/netdeploy/lib/python3.11/site-packages/eventlet/event.py new file mode 100644 index 0000000..122bd5d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/event.py @@ -0,0 +1,218 @@ +from eventlet import hubs +from eventlet.support import greenlets as greenlet + +__all__ = ['Event'] + + +class NOT_USED: + def __repr__(self): + return 'NOT_USED' + + +NOT_USED = NOT_USED() + + +class Event: + """An abstraction where an arbitrary number of coroutines + can wait for one event from another. + + Events are similar to a Queue that can only hold one item, but differ + in two important ways: + + 1. calling :meth:`send` never unschedules the current greenthread + 2. :meth:`send` can only be called once; create a new event to send again. + + They are good for communicating results between coroutines, and + are the basis for how + :meth:`GreenThread.wait() ` + is implemented. + + >>> from eventlet import event + >>> import eventlet + >>> evt = event.Event() + >>> def baz(b): + ... evt.send(b + 1) + ... + >>> _ = eventlet.spawn_n(baz, 3) + >>> evt.wait() + 4 + """ + _result = None + _exc = None + + def __init__(self): + self._waiters = set() + self.reset() + + def __str__(self): + params = (self.__class__.__name__, hex(id(self)), + self._result, self._exc, len(self._waiters)) + return '<%s at %s result=%r _exc=%r _waiters[%d]>' % params + + def reset(self): + # this is kind of a misfeature and doesn't work perfectly well, + # it's better to create a new event rather than reset an old one + # removing documentation so that we don't get new use cases for it + assert self._result is not NOT_USED, 'Trying to re-reset() a fresh event.' + self._result = NOT_USED + self._exc = None + + def ready(self): + """ Return true if the :meth:`wait` call will return immediately. + Used to avoid waiting for things that might take a while to time out. + For example, you can put a bunch of events into a list, and then visit + them all repeatedly, calling :meth:`ready` until one returns ``True``, + and then you can :meth:`wait` on that one.""" + return self._result is not NOT_USED + + def has_exception(self): + return self._exc is not None + + def has_result(self): + return self._result is not NOT_USED and self._exc is None + + def poll(self, notready=None): + if self.ready(): + return self.wait() + return notready + + # QQQ make it return tuple (type, value, tb) instead of raising + # because + # 1) "poll" does not imply raising + # 2) it's better not to screw up caller's sys.exc_info() by default + # (e.g. if caller wants to calls the function in except or finally) + def poll_exception(self, notready=None): + if self.has_exception(): + return self.wait() + return notready + + def poll_result(self, notready=None): + if self.has_result(): + return self.wait() + return notready + + def wait(self, timeout=None): + """Wait until another coroutine calls :meth:`send`. + Returns the value the other coroutine passed to :meth:`send`. + + >>> import eventlet + >>> evt = eventlet.Event() + >>> def wait_on(): + ... retval = evt.wait() + ... print("waited for {0}".format(retval)) + >>> _ = eventlet.spawn(wait_on) + >>> evt.send('result') + >>> eventlet.sleep(0) + waited for result + + Returns immediately if the event has already occurred. + + >>> evt.wait() + 'result' + + When the timeout argument is present and not None, it should be a floating point number + specifying a timeout for the operation in seconds (or fractions thereof). + """ + current = greenlet.getcurrent() + if self._result is NOT_USED: + hub = hubs.get_hub() + self._waiters.add(current) + timer = None + if timeout is not None: + timer = hub.schedule_call_local(timeout, self._do_send, None, None, current) + try: + result = hub.switch() + if timer is not None: + timer.cancel() + return result + finally: + self._waiters.discard(current) + if self._exc is not None: + current.throw(*self._exc) + return self._result + + def send(self, result=None, exc=None): + """Makes arrangements for the waiters to be woken with the + result and then returns immediately to the parent. + + >>> from eventlet import event + >>> import eventlet + >>> evt = event.Event() + >>> def waiter(): + ... print('about to wait') + ... result = evt.wait() + ... print('waited for {0}'.format(result)) + >>> _ = eventlet.spawn(waiter) + >>> eventlet.sleep(0) + about to wait + >>> evt.send('a') + >>> eventlet.sleep(0) + waited for a + + It is an error to call :meth:`send` multiple times on the same event. + + >>> evt.send('whoops') # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + AssertionError: Trying to re-send() an already-triggered event. + + Use :meth:`reset` between :meth:`send` s to reuse an event object. + """ + assert self._result is NOT_USED, 'Trying to re-send() an already-triggered event.' + self._result = result + if exc is not None and not isinstance(exc, tuple): + exc = (exc, ) + self._exc = exc + hub = hubs.get_hub() + for waiter in self._waiters: + hub.schedule_call_global( + 0, self._do_send, self._result, self._exc, waiter) + + def _do_send(self, result, exc, waiter): + if waiter in self._waiters: + if exc is None: + waiter.switch(result) + else: + waiter.throw(*exc) + + def send_exception(self, *args): + """Same as :meth:`send`, but sends an exception to waiters. + + The arguments to send_exception are the same as the arguments + to ``raise``. If a single exception object is passed in, it + will be re-raised when :meth:`wait` is called, generating a + new stacktrace. + + >>> from eventlet import event + >>> evt = event.Event() + >>> evt.send_exception(RuntimeError()) + >>> evt.wait() + Traceback (most recent call last): + File "", line 1, in + File "eventlet/event.py", line 120, in wait + current.throw(*self._exc) + RuntimeError + + If it's important to preserve the entire original stack trace, + you must pass in the entire :func:`sys.exc_info` tuple. + + >>> import sys + >>> evt = event.Event() + >>> try: + ... raise RuntimeError() + ... except RuntimeError: + ... evt.send_exception(*sys.exc_info()) + ... + >>> evt.wait() + Traceback (most recent call last): + File "", line 1, in + File "eventlet/event.py", line 120, in wait + current.throw(*self._exc) + File "", line 2, in + RuntimeError + + Note that doing so stores a traceback object directly on the + Event object, which may cause reference cycles. See the + :func:`sys.exc_info` documentation. + """ + # the arguments and the same as for greenlet.throw + return self.send(None, args) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/BaseHTTPServer.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/BaseHTTPServer.py new file mode 100644 index 0000000..9a73730 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/BaseHTTPServer.py @@ -0,0 +1,15 @@ +from eventlet import patcher +from eventlet.green import socket +from eventlet.green import SocketServer + +patcher.inject( + 'http.server', + globals(), + ('socket', socket), + ('SocketServer', SocketServer), + ('socketserver', SocketServer)) + +del patcher + +if __name__ == '__main__': + test() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/CGIHTTPServer.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/CGIHTTPServer.py new file mode 100644 index 0000000..285b50c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/CGIHTTPServer.py @@ -0,0 +1,17 @@ +from eventlet import patcher +from eventlet.green import BaseHTTPServer +from eventlet.green import SimpleHTTPServer +from eventlet.green import urllib +from eventlet.green import select + +test = None # bind prior to patcher.inject to silence pyflakes warning below +patcher.inject( + 'http.server', + globals(), + ('urllib', urllib), + ('select', select)) + +del patcher + +if __name__ == '__main__': + test() # pyflakes false alarm here unless test = None above diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/MySQLdb.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/MySQLdb.py new file mode 100644 index 0000000..16a7ec5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/MySQLdb.py @@ -0,0 +1,40 @@ +__MySQLdb = __import__('MySQLdb') + +__all__ = __MySQLdb.__all__ +__patched__ = ["connect", "Connect", 'Connection', 'connections'] + +from eventlet.patcher import slurp_properties +slurp_properties( + __MySQLdb, globals(), + ignore=__patched__, srckeys=dir(__MySQLdb)) + +from eventlet import tpool + +__orig_connections = __import__('MySQLdb.connections').connections + + +def Connection(*args, **kw): + conn = tpool.execute(__orig_connections.Connection, *args, **kw) + return tpool.Proxy(conn, autowrap_names=('cursor',)) + + +connect = Connect = Connection + + +# replicate the MySQLdb.connections module but with a tpooled Connection factory +class MySQLdbConnectionsModule: + pass + + +connections = MySQLdbConnectionsModule() +for var in dir(__orig_connections): + if not var.startswith('__'): + setattr(connections, var, getattr(__orig_connections, var)) +connections.Connection = Connection + +cursors = __import__('MySQLdb.cursors').cursors +converters = __import__('MySQLdb.converters').converters + +# TODO support instantiating cursors.FooCursor objects directly +# TODO though this is a low priority, it would be nice if we supported +# subclassing eventlet.green.MySQLdb.connections.Connection diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/SSL.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/SSL.py new file mode 100644 index 0000000..bb06c8b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/SSL.py @@ -0,0 +1,125 @@ +from OpenSSL import SSL as orig_SSL +from OpenSSL.SSL import * +from eventlet.support import get_errno +from eventlet import greenio +from eventlet.hubs import trampoline +import socket + + +class GreenConnection(greenio.GreenSocket): + """ Nonblocking wrapper for SSL.Connection objects. + """ + + def __init__(self, ctx, sock=None): + if sock is not None: + fd = orig_SSL.Connection(ctx, sock) + else: + # if we're given a Connection object directly, use it; + # this is used in the inherited accept() method + fd = ctx + super(ConnectionType, self).__init__(fd) + + def do_handshake(self): + """ Perform an SSL handshake (usually called after renegotiate or one of + set_accept_state or set_accept_state). This can raise the same exceptions as + send and recv. """ + if self.act_non_blocking: + return self.fd.do_handshake() + while True: + try: + return self.fd.do_handshake() + except WantReadError: + trampoline(self.fd.fileno(), + read=True, + timeout=self.gettimeout(), + timeout_exc=socket.timeout) + except WantWriteError: + trampoline(self.fd.fileno(), + write=True, + timeout=self.gettimeout(), + timeout_exc=socket.timeout) + + def dup(self): + raise NotImplementedError("Dup not supported on SSL sockets") + + def makefile(self, mode='r', bufsize=-1): + raise NotImplementedError("Makefile not supported on SSL sockets") + + def read(self, size): + """Works like a blocking call to SSL_read(), whose behavior is + described here: http://www.openssl.org/docs/ssl/SSL_read.html""" + if self.act_non_blocking: + return self.fd.read(size) + while True: + try: + return self.fd.read(size) + except WantReadError: + trampoline(self.fd.fileno(), + read=True, + timeout=self.gettimeout(), + timeout_exc=socket.timeout) + except WantWriteError: + trampoline(self.fd.fileno(), + write=True, + timeout=self.gettimeout(), + timeout_exc=socket.timeout) + except SysCallError as e: + if get_errno(e) == -1 or get_errno(e) > 0: + return '' + + recv = read + + def write(self, data): + """Works like a blocking call to SSL_write(), whose behavior is + described here: http://www.openssl.org/docs/ssl/SSL_write.html""" + if not data: + return 0 # calling SSL_write() with 0 bytes to be sent is undefined + if self.act_non_blocking: + return self.fd.write(data) + while True: + try: + return self.fd.write(data) + except WantReadError: + trampoline(self.fd.fileno(), + read=True, + timeout=self.gettimeout(), + timeout_exc=socket.timeout) + except WantWriteError: + trampoline(self.fd.fileno(), + write=True, + timeout=self.gettimeout(), + timeout_exc=socket.timeout) + + send = write + + def sendall(self, data): + """Send "all" data on the connection. This calls send() repeatedly until + all data is sent. If an error occurs, it's impossible to tell how much data + has been sent. + + No return value.""" + tail = self.send(data) + while tail < len(data): + tail += self.send(data[tail:]) + + def shutdown(self): + if self.act_non_blocking: + return self.fd.shutdown() + while True: + try: + return self.fd.shutdown() + except WantReadError: + trampoline(self.fd.fileno(), + read=True, + timeout=self.gettimeout(), + timeout_exc=socket.timeout) + except WantWriteError: + trampoline(self.fd.fileno(), + write=True, + timeout=self.gettimeout(), + timeout_exc=socket.timeout) + + +Connection = ConnectionType = GreenConnection + +del greenio diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/__init__.py new file mode 100644 index 0000000..1b25009 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/__init__.py @@ -0,0 +1,9 @@ +from . import crypto +from . import SSL +try: + # pyopenssl tsafe module was deprecated and removed in v20.0.0 + # https://github.com/pyca/pyopenssl/pull/913 + from . import tsafe +except ImportError: + pass +from .version import __version__ diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/crypto.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/crypto.py new file mode 100644 index 0000000..0a57f6f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/crypto.py @@ -0,0 +1 @@ +from OpenSSL.crypto import * diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/tsafe.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/tsafe.py new file mode 100644 index 0000000..dd0dd8c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/tsafe.py @@ -0,0 +1 @@ +from OpenSSL.tsafe import * diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/version.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/version.py new file mode 100644 index 0000000..c886ef0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/OpenSSL/version.py @@ -0,0 +1 @@ +from OpenSSL.version import __version__, __doc__ diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/Queue.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/Queue.py new file mode 100644 index 0000000..947d43a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/Queue.py @@ -0,0 +1,33 @@ +from eventlet import queue + +__all__ = ['Empty', 'Full', 'LifoQueue', 'PriorityQueue', 'Queue'] + +__patched__ = ['LifoQueue', 'PriorityQueue', 'Queue'] + +# these classes exist to paper over the major operational difference between +# eventlet.queue.Queue and the stdlib equivalents + + +class Queue(queue.Queue): + def __init__(self, maxsize=0): + if maxsize == 0: + maxsize = None + super().__init__(maxsize) + + +class PriorityQueue(queue.PriorityQueue): + def __init__(self, maxsize=0): + if maxsize == 0: + maxsize = None + super().__init__(maxsize) + + +class LifoQueue(queue.LifoQueue): + def __init__(self, maxsize=0): + if maxsize == 0: + maxsize = None + super().__init__(maxsize) + + +Empty = queue.Empty +Full = queue.Full diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/SimpleHTTPServer.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/SimpleHTTPServer.py new file mode 100644 index 0000000..df49fc9 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/SimpleHTTPServer.py @@ -0,0 +1,13 @@ +from eventlet import patcher +from eventlet.green import BaseHTTPServer +from eventlet.green import urllib + +patcher.inject( + 'http.server', + globals(), + ('urllib', urllib)) + +del patcher + +if __name__ == '__main__': + test() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/SocketServer.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/SocketServer.py new file mode 100644 index 0000000..b94ead3 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/SocketServer.py @@ -0,0 +1,14 @@ +from eventlet import patcher + +from eventlet.green import socket +from eventlet.green import select +from eventlet.green import threading + +patcher.inject( + 'socketserver', + globals(), + ('socket', socket), + ('select', select), + ('threading', threading)) + +# QQQ ForkingMixIn should be fixed to use green waitpid? diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/__init__.py new file mode 100644 index 0000000..d965325 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/__init__.py @@ -0,0 +1 @@ +# this package contains modules from the standard library converted to use eventlet diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/_socket_nodns.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/_socket_nodns.py new file mode 100644 index 0000000..7dca20a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/_socket_nodns.py @@ -0,0 +1,33 @@ +__socket = __import__('socket') + +__all__ = __socket.__all__ +__patched__ = ['fromfd', 'socketpair', 'ssl', 'socket', 'timeout'] + +import eventlet.patcher +eventlet.patcher.slurp_properties(__socket, globals(), ignore=__patched__, srckeys=dir(__socket)) + +os = __import__('os') +import sys +from eventlet import greenio + + +socket = greenio.GreenSocket +_GLOBAL_DEFAULT_TIMEOUT = greenio._GLOBAL_DEFAULT_TIMEOUT +timeout = greenio.socket_timeout + +try: + __original_fromfd__ = __socket.fromfd + + def fromfd(*args): + return socket(__original_fromfd__(*args)) +except AttributeError: + pass + +try: + __original_socketpair__ = __socket.socketpair + + def socketpair(*args): + one, two = __original_socketpair__(*args) + return socket(one), socket(two) +except AttributeError: + pass diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/asynchat.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/asynchat.py new file mode 100644 index 0000000..da51396 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/asynchat.py @@ -0,0 +1,14 @@ +import sys + +if sys.version_info < (3, 12): + from eventlet import patcher + from eventlet.green import asyncore + from eventlet.green import socket + + patcher.inject( + 'asynchat', + globals(), + ('asyncore', asyncore), + ('socket', socket)) + + del patcher diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/asyncore.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/asyncore.py new file mode 100644 index 0000000..e7a7959 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/asyncore.py @@ -0,0 +1,16 @@ +import sys + +if sys.version_info < (3, 12): + from eventlet import patcher + from eventlet.green import select + from eventlet.green import socket + from eventlet.green import time + + patcher.inject( + "asyncore", + globals(), + ('select', select), + ('socket', socket), + ('time', time)) + + del patcher diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/builtin.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/builtin.py new file mode 100644 index 0000000..ce98290 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/builtin.py @@ -0,0 +1,38 @@ +""" +In order to detect a filehandle that's been closed, our only clue may be +the operating system returning the same filehandle in response to some +other operation. + +The builtins 'file' and 'open' are patched to collaborate with the +notify_opened protocol. +""" + +builtins_orig = __builtins__ + +from eventlet import hubs +from eventlet.hubs import hub +from eventlet.patcher import slurp_properties +import sys + +__all__ = dir(builtins_orig) +__patched__ = ['open'] +slurp_properties(builtins_orig, globals(), + ignore=__patched__, srckeys=dir(builtins_orig)) + +hubs.get_hub() + +__original_open = open +__opening = False + + +def open(*args, **kwargs): + global __opening + result = __original_open(*args, **kwargs) + if not __opening: + # This is incredibly ugly. 'open' is used under the hood by + # the import process. So, ensure we don't wind up in an + # infinite loop. + __opening = True + hubs.notify_opened(result.fileno()) + __opening = False + return result diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/ftplib.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/ftplib.py new file mode 100644 index 0000000..b452e1d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/ftplib.py @@ -0,0 +1,13 @@ +from eventlet import patcher + +# *NOTE: there might be some funny business with the "SOCKS" module +# if it even still exists +from eventlet.green import socket + +patcher.inject('ftplib', globals(), ('socket', socket)) + +del patcher + +# Run test program when run as a script +if __name__ == '__main__': + test() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/http/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/http/__init__.py new file mode 100644 index 0000000..14e74fd --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/http/__init__.py @@ -0,0 +1,189 @@ +# This is part of Python source code with Eventlet-specific modifications. +# +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights +# Reserved +# +# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +# -------------------------------------------- +# +# 1. This LICENSE AGREEMENT is between the Python Software Foundation +# ("PSF"), and the Individual or Organization ("Licensee") accessing and +# otherwise using this software ("Python") in source or binary form and +# its associated documentation. +# +# 2. Subject to the terms and conditions of this License Agreement, PSF hereby +# grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +# analyze, test, perform and/or display publicly, prepare derivative works, +# distribute, and otherwise use Python alone or in any derivative version, +# provided, however, that PSF's License Agreement and PSF's notice of copyright, +# i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights +# Reserved" are retained in Python alone or in any derivative version prepared by +# Licensee. +# +# 3. In the event Licensee prepares a derivative work that is based on +# or incorporates Python or any part thereof, and wants to make +# the derivative work available to others as provided herein, then +# Licensee hereby agrees to include in any such work a brief summary of +# the changes made to Python. +# +# 4. PSF is making Python available to Licensee on an "AS IS" +# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +# INFRINGE ANY THIRD PARTY RIGHTS. +# +# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. +# +# 6. This License Agreement will automatically terminate upon a material +# breach of its terms and conditions. +# +# 7. Nothing in this License Agreement shall be deemed to create any +# relationship of agency, partnership, or joint venture between PSF and +# Licensee. This License Agreement does not grant permission to use PSF +# trademarks or trade name in a trademark sense to endorse or promote +# products or services of Licensee, or any third party. +# +# 8. By copying, installing or otherwise using Python, Licensee +# agrees to be bound by the terms and conditions of this License +# Agreement. + +from enum import IntEnum + +__all__ = ['HTTPStatus'] + +class HTTPStatus(IntEnum): + """HTTP status codes and reason phrases + + Status codes from the following RFCs are all observed: + + * RFC 7231: Hypertext Transfer Protocol (HTTP/1.1), obsoletes 2616 + * RFC 6585: Additional HTTP Status Codes + * RFC 3229: Delta encoding in HTTP + * RFC 4918: HTTP Extensions for WebDAV, obsoletes 2518 + * RFC 5842: Binding Extensions to WebDAV + * RFC 7238: Permanent Redirect + * RFC 2295: Transparent Content Negotiation in HTTP + * RFC 2774: An HTTP Extension Framework + """ + def __new__(cls, value, phrase, description=''): + obj = int.__new__(cls, value) + obj._value_ = value + + obj.phrase = phrase + obj.description = description + return obj + + # informational + CONTINUE = 100, 'Continue', 'Request received, please continue' + SWITCHING_PROTOCOLS = (101, 'Switching Protocols', + 'Switching to new protocol; obey Upgrade header') + PROCESSING = 102, 'Processing' + + # success + OK = 200, 'OK', 'Request fulfilled, document follows' + CREATED = 201, 'Created', 'Document created, URL follows' + ACCEPTED = (202, 'Accepted', + 'Request accepted, processing continues off-line') + NON_AUTHORITATIVE_INFORMATION = (203, + 'Non-Authoritative Information', 'Request fulfilled from cache') + NO_CONTENT = 204, 'No Content', 'Request fulfilled, nothing follows' + RESET_CONTENT = 205, 'Reset Content', 'Clear input form for further input' + PARTIAL_CONTENT = 206, 'Partial Content', 'Partial content follows' + MULTI_STATUS = 207, 'Multi-Status' + ALREADY_REPORTED = 208, 'Already Reported' + IM_USED = 226, 'IM Used' + + # redirection + MULTIPLE_CHOICES = (300, 'Multiple Choices', + 'Object has several resources -- see URI list') + MOVED_PERMANENTLY = (301, 'Moved Permanently', + 'Object moved permanently -- see URI list') + FOUND = 302, 'Found', 'Object moved temporarily -- see URI list' + SEE_OTHER = 303, 'See Other', 'Object moved -- see Method and URL list' + NOT_MODIFIED = (304, 'Not Modified', + 'Document has not changed since given time') + USE_PROXY = (305, 'Use Proxy', + 'You must use proxy specified in Location to access this resource') + TEMPORARY_REDIRECT = (307, 'Temporary Redirect', + 'Object moved temporarily -- see URI list') + PERMANENT_REDIRECT = (308, 'Permanent Redirect', + 'Object moved temporarily -- see URI list') + + # client error + BAD_REQUEST = (400, 'Bad Request', + 'Bad request syntax or unsupported method') + UNAUTHORIZED = (401, 'Unauthorized', + 'No permission -- see authorization schemes') + PAYMENT_REQUIRED = (402, 'Payment Required', + 'No payment -- see charging schemes') + FORBIDDEN = (403, 'Forbidden', + 'Request forbidden -- authorization will not help') + NOT_FOUND = (404, 'Not Found', + 'Nothing matches the given URI') + METHOD_NOT_ALLOWED = (405, 'Method Not Allowed', + 'Specified method is invalid for this resource') + NOT_ACCEPTABLE = (406, 'Not Acceptable', + 'URI not available in preferred format') + PROXY_AUTHENTICATION_REQUIRED = (407, + 'Proxy Authentication Required', + 'You must authenticate with this proxy before proceeding') + REQUEST_TIMEOUT = (408, 'Request Timeout', + 'Request timed out; try again later') + CONFLICT = 409, 'Conflict', 'Request conflict' + GONE = (410, 'Gone', + 'URI no longer exists and has been permanently removed') + LENGTH_REQUIRED = (411, 'Length Required', + 'Client must specify Content-Length') + PRECONDITION_FAILED = (412, 'Precondition Failed', + 'Precondition in headers is false') + REQUEST_ENTITY_TOO_LARGE = (413, 'Request Entity Too Large', + 'Entity is too large') + REQUEST_URI_TOO_LONG = (414, 'Request-URI Too Long', + 'URI is too long') + UNSUPPORTED_MEDIA_TYPE = (415, 'Unsupported Media Type', + 'Entity body in unsupported format') + REQUESTED_RANGE_NOT_SATISFIABLE = (416, + 'Requested Range Not Satisfiable', + 'Cannot satisfy request range') + EXPECTATION_FAILED = (417, 'Expectation Failed', + 'Expect condition could not be satisfied') + UNPROCESSABLE_ENTITY = 422, 'Unprocessable Entity' + LOCKED = 423, 'Locked' + FAILED_DEPENDENCY = 424, 'Failed Dependency' + UPGRADE_REQUIRED = 426, 'Upgrade Required' + PRECONDITION_REQUIRED = (428, 'Precondition Required', + 'The origin server requires the request to be conditional') + TOO_MANY_REQUESTS = (429, 'Too Many Requests', + 'The user has sent too many requests in ' + 'a given amount of time ("rate limiting")') + REQUEST_HEADER_FIELDS_TOO_LARGE = (431, + 'Request Header Fields Too Large', + 'The server is unwilling to process the request because its header ' + 'fields are too large') + + # server errors + INTERNAL_SERVER_ERROR = (500, 'Internal Server Error', + 'Server got itself in trouble') + NOT_IMPLEMENTED = (501, 'Not Implemented', + 'Server does not support this operation') + BAD_GATEWAY = (502, 'Bad Gateway', + 'Invalid responses from another server/proxy') + SERVICE_UNAVAILABLE = (503, 'Service Unavailable', + 'The server cannot process the request due to a high load') + GATEWAY_TIMEOUT = (504, 'Gateway Timeout', + 'The gateway server did not receive a timely response') + HTTP_VERSION_NOT_SUPPORTED = (505, 'HTTP Version Not Supported', + 'Cannot fulfill request') + VARIANT_ALSO_NEGOTIATES = 506, 'Variant Also Negotiates' + INSUFFICIENT_STORAGE = 507, 'Insufficient Storage' + LOOP_DETECTED = 508, 'Loop Detected' + NOT_EXTENDED = 510, 'Not Extended' + NETWORK_AUTHENTICATION_REQUIRED = (511, + 'Network Authentication Required', + 'The client needs to authenticate to gain network access') diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/http/client.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/http/client.py new file mode 100644 index 0000000..2051ca9 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/http/client.py @@ -0,0 +1,1578 @@ +# This is part of Python source code with Eventlet-specific modifications. +# +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights +# Reserved +# +# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +# -------------------------------------------- +# +# 1. This LICENSE AGREEMENT is between the Python Software Foundation +# ("PSF"), and the Individual or Organization ("Licensee") accessing and +# otherwise using this software ("Python") in source or binary form and +# its associated documentation. +# +# 2. Subject to the terms and conditions of this License Agreement, PSF hereby +# grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +# analyze, test, perform and/or display publicly, prepare derivative works, +# distribute, and otherwise use Python alone or in any derivative version, +# provided, however, that PSF's License Agreement and PSF's notice of copyright, +# i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights +# Reserved" are retained in Python alone or in any derivative version prepared by +# Licensee. +# +# 3. In the event Licensee prepares a derivative work that is based on +# or incorporates Python or any part thereof, and wants to make +# the derivative work available to others as provided herein, then +# Licensee hereby agrees to include in any such work a brief summary of +# the changes made to Python. +# +# 4. PSF is making Python available to Licensee on an "AS IS" +# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +# INFRINGE ANY THIRD PARTY RIGHTS. +# +# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. +# +# 6. This License Agreement will automatically terminate upon a material +# breach of its terms and conditions. +# +# 7. Nothing in this License Agreement shall be deemed to create any +# relationship of agency, partnership, or joint venture between PSF and +# Licensee. This License Agreement does not grant permission to use PSF +# trademarks or trade name in a trademark sense to endorse or promote +# products or services of Licensee, or any third party. +# +# 8. By copying, installing or otherwise using Python, Licensee +# agrees to be bound by the terms and conditions of this License +# Agreement. +"""HTTP/1.1 client library + + + + +HTTPConnection goes through a number of "states", which define when a client +may legally make another request or fetch the response for a particular +request. This diagram details these state transitions: + + (null) + | + | HTTPConnection() + v + Idle + | + | putrequest() + v + Request-started + | + | ( putheader() )* endheaders() + v + Request-sent + |\\_____________________________ + | | getresponse() raises + | response = getresponse() | ConnectionError + v v + Unread-response Idle + [Response-headers-read] + |\\____________________ + | | + | response.read() | putrequest() + v v + Idle Req-started-unread-response + ______/| + / | + response.read() | | ( putheader() )* endheaders() + v v + Request-started Req-sent-unread-response + | + | response.read() + v + Request-sent + +This diagram presents the following rules: + -- a second request may not be started until {response-headers-read} + -- a response [object] cannot be retrieved until {request-sent} + -- there is no differentiation between an unread response body and a + partially read response body + +Note: this enforcement is applied by the HTTPConnection class. The + HTTPResponse class does not enforce this state machine, which + implies sophisticated clients may accelerate the request/response + pipeline. Caution should be taken, though: accelerating the states + beyond the above pattern may imply knowledge of the server's + connection-close behavior for certain requests. For example, it + is impossible to tell whether the server will close the connection + UNTIL the response headers have been read; this means that further + requests cannot be placed into the pipeline until it is known that + the server will NOT be closing the connection. + +Logical State __state __response +------------- ------- ---------- +Idle _CS_IDLE None +Request-started _CS_REQ_STARTED None +Request-sent _CS_REQ_SENT None +Unread-response _CS_IDLE +Req-started-unread-response _CS_REQ_STARTED +Req-sent-unread-response _CS_REQ_SENT +""" + +import email.parser +import email.message +import io +import re +from collections.abc import Iterable +from urllib.parse import urlsplit + +from eventlet.green import http, os, socket + +# HTTPMessage, parse_headers(), and the HTTP status code constants are +# intentionally omitted for simplicity +__all__ = ["HTTPResponse", "HTTPConnection", + "HTTPException", "NotConnected", "UnknownProtocol", + "UnknownTransferEncoding", "UnimplementedFileMode", + "IncompleteRead", "InvalidURL", "ImproperConnectionState", + "CannotSendRequest", "CannotSendHeader", "ResponseNotReady", + "BadStatusLine", "LineTooLong", "RemoteDisconnected", "error", + "responses"] + +HTTP_PORT = 80 +HTTPS_PORT = 443 + +_UNKNOWN = 'UNKNOWN' + +# connection states +_CS_IDLE = 'Idle' +_CS_REQ_STARTED = 'Request-started' +_CS_REQ_SENT = 'Request-sent' + + +# hack to maintain backwards compatibility +globals().update(http.HTTPStatus.__members__) + +# another hack to maintain backwards compatibility +# Mapping status codes to official W3C names +responses = {v: v.phrase for v in http.HTTPStatus.__members__.values()} + +# maximal amount of data to read at one time in _safe_read +MAXAMOUNT = 1048576 + +# maximal line length when calling readline(). +_MAXLINE = 65536 +_MAXHEADERS = 100 + +# Header name/value ABNF (http://tools.ietf.org/html/rfc7230#section-3.2) +# +# VCHAR = %x21-7E +# obs-text = %x80-FF +# header-field = field-name ":" OWS field-value OWS +# field-name = token +# field-value = *( field-content / obs-fold ) +# field-content = field-vchar [ 1*( SP / HTAB ) field-vchar ] +# field-vchar = VCHAR / obs-text +# +# obs-fold = CRLF 1*( SP / HTAB ) +# ; obsolete line folding +# ; see Section 3.2.4 + +# token = 1*tchar +# +# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" +# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" +# / DIGIT / ALPHA +# ; any VCHAR, except delimiters +# +# VCHAR defined in http://tools.ietf.org/html/rfc5234#appendix-B.1 + +# the patterns for both name and value are more leniant than RFC +# definitions to allow for backwards compatibility +# Eventlet change: match used instead of fullmatch for Python 3.3 compatibility +_is_legal_header_name = re.compile(rb'[^:\s][^:\r\n]*\Z').match +_is_illegal_header_value = re.compile(rb'\n(?![ \t])|\r(?![ \t\n])').search + +# We always set the Content-Length header for these methods because some +# servers will otherwise respond with a 411 +_METHODS_EXPECTING_BODY = {'PATCH', 'POST', 'PUT'} + + +def _encode(data, name='data'): + """Call data.encode("latin-1") but show a better error message.""" + try: + return data.encode("latin-1") + except UnicodeEncodeError as err: + raise UnicodeEncodeError( + err.encoding, + err.object, + err.start, + err.end, + "%s (%.20r) is not valid Latin-1. Use %s.encode('utf-8') " + "if you want to send it encoded in UTF-8." % + (name.title(), data[err.start:err.end], name)) from None + + +class HTTPMessage(email.message.Message): + # XXX The only usage of this method is in + # http.server.CGIHTTPRequestHandler. Maybe move the code there so + # that it doesn't need to be part of the public API. The API has + # never been defined so this could cause backwards compatibility + # issues. + + def getallmatchingheaders(self, name): + """Find all header lines matching a given header name. + + Look through the list of headers and find all lines matching a given + header name (and their continuation lines). A list of the lines is + returned, without interpretation. If the header does not occur, an + empty list is returned. If the header occurs multiple times, all + occurrences are returned. Case is not important in the header name. + + """ + name = name.lower() + ':' + n = len(name) + lst = [] + hit = 0 + for line in self.keys(): + if line[:n].lower() == name: + hit = 1 + elif not line[:1].isspace(): + hit = 0 + if hit: + lst.append(line) + return lst + +def parse_headers(fp, _class=HTTPMessage): + """Parses only RFC2822 headers from a file pointer. + + email Parser wants to see strings rather than bytes. + But a TextIOWrapper around self.rfile would buffer too many bytes + from the stream, bytes which we later need to read as bytes. + So we read the correct bytes here, as bytes, for email Parser + to parse. + + """ + headers = [] + while True: + line = fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise LineTooLong("header line") + headers.append(line) + if len(headers) > _MAXHEADERS: + raise HTTPException("got more than %d headers" % _MAXHEADERS) + if line in (b'\r\n', b'\n', b''): + break + hstring = b''.join(headers).decode('iso-8859-1') + return email.parser.Parser(_class=_class).parsestr(hstring) + + +class HTTPResponse(io.BufferedIOBase): + + # See RFC 2616 sec 19.6 and RFC 1945 sec 6 for details. + + # The bytes from the socket object are iso-8859-1 strings. + # See RFC 2616 sec 2.2 which notes an exception for MIME-encoded + # text following RFC 2047. The basic status line parsing only + # accepts iso-8859-1. + + def __init__(self, sock, debuglevel=0, method=None, url=None): + # If the response includes a content-length header, we need to + # make sure that the client doesn't read more than the + # specified number of bytes. If it does, it will block until + # the server times out and closes the connection. This will + # happen if a self.fp.read() is done (without a size) whether + # self.fp is buffered or not. So, no self.fp.read() by + # clients unless they know what they are doing. + self.fp = sock.makefile("rb") + self.debuglevel = debuglevel + self._method = method + + # The HTTPResponse object is returned via urllib. The clients + # of http and urllib expect different attributes for the + # headers. headers is used here and supports urllib. msg is + # provided as a backwards compatibility layer for http + # clients. + + self.headers = self.msg = None + + # from the Status-Line of the response + self.version = _UNKNOWN # HTTP-Version + self.status = _UNKNOWN # Status-Code + self.reason = _UNKNOWN # Reason-Phrase + + self.chunked = _UNKNOWN # is "chunked" being used? + self.chunk_left = _UNKNOWN # bytes left to read in current chunk + self.length = _UNKNOWN # number of bytes left in response + self.will_close = _UNKNOWN # conn will close at end of response + + def _read_status(self): + line = str(self.fp.readline(_MAXLINE + 1), "iso-8859-1") + if len(line) > _MAXLINE: + raise LineTooLong("status line") + if self.debuglevel > 0: + print("reply:", repr(line)) + if not line: + # Presumably, the server closed the connection before + # sending a valid response. + raise RemoteDisconnected("Remote end closed connection without" + " response") + try: + version, status, reason = line.split(None, 2) + except ValueError: + try: + version, status = line.split(None, 1) + reason = "" + except ValueError: + # empty version will cause next test to fail. + version = "" + if not version.startswith("HTTP/"): + self._close_conn() + raise BadStatusLine(line) + + # The status code is a three-digit number + try: + status = int(status) + if status < 100 or status > 999: + raise BadStatusLine(line) + except ValueError: + raise BadStatusLine(line) + return version, status, reason + + def begin(self): + if self.headers is not None: + # we've already started reading the response + return + + # read until we get a non-100 response + while True: + version, status, reason = self._read_status() + if status != CONTINUE: + break + # skip the header from the 100 response + while True: + skip = self.fp.readline(_MAXLINE + 1) + if len(skip) > _MAXLINE: + raise LineTooLong("header line") + skip = skip.strip() + if not skip: + break + if self.debuglevel > 0: + print("header:", skip) + + self.code = self.status = status + self.reason = reason.strip() + if version in ("HTTP/1.0", "HTTP/0.9"): + # Some servers might still return "0.9", treat it as 1.0 anyway + self.version = 10 + elif version.startswith("HTTP/1."): + self.version = 11 # use HTTP/1.1 code for HTTP/1.x where x>=1 + else: + raise UnknownProtocol(version) + + self.headers = self.msg = parse_headers(self.fp) + + if self.debuglevel > 0: + for hdr in self.headers: + print("header:", hdr, end=" ") + + # are we using the chunked-style of transfer encoding? + tr_enc = self.headers.get("transfer-encoding") + if tr_enc and tr_enc.lower() == "chunked": + self.chunked = True + self.chunk_left = None + else: + self.chunked = False + + # will the connection close at the end of the response? + self.will_close = self._check_close() + + # do we have a Content-Length? + # NOTE: RFC 2616, S4.4, #3 says we ignore this if tr_enc is "chunked" + self.length = None + length = self.headers.get("content-length") + + # are we using the chunked-style of transfer encoding? + tr_enc = self.headers.get("transfer-encoding") + if length and not self.chunked: + try: + self.length = int(length) + except ValueError: + self.length = None + else: + if self.length < 0: # ignore nonsensical negative lengths + self.length = None + else: + self.length = None + + # does the body have a fixed length? (of zero) + if (status == NO_CONTENT or status == NOT_MODIFIED or + 100 <= status < 200 or # 1xx codes + self._method == "HEAD"): + self.length = 0 + + # if the connection remains open, and we aren't using chunked, and + # a content-length was not provided, then assume that the connection + # WILL close. + if (not self.will_close and + not self.chunked and + self.length is None): + self.will_close = True + + def _check_close(self): + conn = self.headers.get("connection") + if self.version == 11: + # An HTTP/1.1 proxy is assumed to stay open unless + # explicitly closed. + conn = self.headers.get("connection") + if conn and "close" in conn.lower(): + return True + return False + + # Some HTTP/1.0 implementations have support for persistent + # connections, using rules different than HTTP/1.1. + + # For older HTTP, Keep-Alive indicates persistent connection. + if self.headers.get("keep-alive"): + return False + + # At least Akamai returns a "Connection: Keep-Alive" header, + # which was supposed to be sent by the client. + if conn and "keep-alive" in conn.lower(): + return False + + # Proxy-Connection is a netscape hack. + pconn = self.headers.get("proxy-connection") + if pconn and "keep-alive" in pconn.lower(): + return False + + # otherwise, assume it will close + return True + + def _close_conn(self): + fp = self.fp + self.fp = None + fp.close() + + def close(self): + try: + super().close() # set "closed" flag + finally: + if self.fp: + self._close_conn() + + # These implementations are for the benefit of io.BufferedReader. + + # XXX This class should probably be revised to act more like + # the "raw stream" that BufferedReader expects. + + def flush(self): + super().flush() + if self.fp: + self.fp.flush() + + def readable(self): + """Always returns True""" + return True + + # End of "raw stream" methods + + def isclosed(self): + """True if the connection is closed.""" + # NOTE: it is possible that we will not ever call self.close(). This + # case occurs when will_close is TRUE, length is None, and we + # read up to the last byte, but NOT past it. + # + # IMPLIES: if will_close is FALSE, then self.close() will ALWAYS be + # called, meaning self.isclosed() is meaningful. + return self.fp is None + + def read(self, amt=None): + if self.fp is None: + return b"" + + if self._method == "HEAD": + self._close_conn() + return b"" + + if amt is not None: + # Amount is given, implement using readinto + b = bytearray(amt) + n = self.readinto(b) + return memoryview(b)[:n].tobytes() + else: + # Amount is not given (unbounded read) so we must check self.length + # and self.chunked + + if self.chunked: + return self._readall_chunked() + + if self.length is None: + s = self.fp.read() + else: + try: + s = self._safe_read(self.length) + except IncompleteRead: + self._close_conn() + raise + self.length = 0 + self._close_conn() # we read everything + return s + + def readinto(self, b): + """Read up to len(b) bytes into bytearray b and return the number + of bytes read. + """ + + if self.fp is None: + return 0 + + if self._method == "HEAD": + self._close_conn() + return 0 + + if self.chunked: + return self._readinto_chunked(b) + + if self.length is not None: + if len(b) > self.length: + # clip the read to the "end of response" + b = memoryview(b)[0:self.length] + + # we do not use _safe_read() here because this may be a .will_close + # connection, and the user is reading more bytes than will be provided + # (for example, reading in 1k chunks) + n = self.fp.readinto(b) + if not n and b: + # Ideally, we would raise IncompleteRead if the content-length + # wasn't satisfied, but it might break compatibility. + self._close_conn() + elif self.length is not None: + self.length -= n + if not self.length: + self._close_conn() + return n + + def _read_next_chunk_size(self): + # Read the next chunk size from the file + line = self.fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise LineTooLong("chunk size") + i = line.find(b";") + if i >= 0: + line = line[:i] # strip chunk-extensions + try: + return int(line, 16) + except ValueError: + # close the connection as protocol synchronisation is + # probably lost + self._close_conn() + raise + + def _read_and_discard_trailer(self): + # read and discard trailer up to the CRLF terminator + ### note: we shouldn't have any trailers! + while True: + line = self.fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise LineTooLong("trailer line") + if not line: + # a vanishingly small number of sites EOF without + # sending the trailer + break + if line in (b'\r\n', b'\n', b''): + break + + def _get_chunk_left(self): + # return self.chunk_left, reading a new chunk if necessary. + # chunk_left == 0: at the end of the current chunk, need to close it + # chunk_left == None: No current chunk, should read next. + # This function returns non-zero or None if the last chunk has + # been read. + chunk_left = self.chunk_left + if not chunk_left: # Can be 0 or None + if chunk_left is not None: + # We are at the end of chunk. dicard chunk end + self._safe_read(2) # toss the CRLF at the end of the chunk + try: + chunk_left = self._read_next_chunk_size() + except ValueError: + raise IncompleteRead(b'') + if chunk_left == 0: + # last chunk: 1*("0") [ chunk-extension ] CRLF + self._read_and_discard_trailer() + # we read everything; close the "file" + self._close_conn() + chunk_left = None + self.chunk_left = chunk_left + return chunk_left + + def _readall_chunked(self): + assert self.chunked != _UNKNOWN + value = [] + try: + while True: + chunk_left = self._get_chunk_left() + if chunk_left is None: + break + value.append(self._safe_read(chunk_left)) + self.chunk_left = 0 + return b''.join(value) + except IncompleteRead: + raise IncompleteRead(b''.join(value)) + + def _readinto_chunked(self, b): + assert self.chunked != _UNKNOWN + total_bytes = 0 + mvb = memoryview(b) + try: + while True: + chunk_left = self._get_chunk_left() + if chunk_left is None: + return total_bytes + + if len(mvb) <= chunk_left: + n = self._safe_readinto(mvb) + self.chunk_left = chunk_left - n + return total_bytes + n + + temp_mvb = mvb[:chunk_left] + n = self._safe_readinto(temp_mvb) + mvb = mvb[n:] + total_bytes += n + self.chunk_left = 0 + + except IncompleteRead: + raise IncompleteRead(bytes(b[0:total_bytes])) + + def _safe_read(self, amt): + """Read the number of bytes requested, compensating for partial reads. + + Normally, we have a blocking socket, but a read() can be interrupted + by a signal (resulting in a partial read). + + Note that we cannot distinguish between EOF and an interrupt when zero + bytes have been read. IncompleteRead() will be raised in this + situation. + + This function should be used when bytes "should" be present for + reading. If the bytes are truly not available (due to EOF), then the + IncompleteRead exception can be used to detect the problem. + """ + s = [] + while amt > 0: + chunk = self.fp.read(min(amt, MAXAMOUNT)) + if not chunk: + raise IncompleteRead(b''.join(s), amt) + s.append(chunk) + amt -= len(chunk) + return b"".join(s) + + def _safe_readinto(self, b): + """Same as _safe_read, but for reading into a buffer.""" + total_bytes = 0 + mvb = memoryview(b) + while total_bytes < len(b): + if MAXAMOUNT < len(mvb): + temp_mvb = mvb[0:MAXAMOUNT] + n = self.fp.readinto(temp_mvb) + else: + n = self.fp.readinto(mvb) + if not n: + raise IncompleteRead(bytes(mvb[0:total_bytes]), len(b)) + mvb = mvb[n:] + total_bytes += n + return total_bytes + + def read1(self, n=-1): + """Read with at most one underlying system call. If at least one + byte is buffered, return that instead. + """ + if self.fp is None or self._method == "HEAD": + return b"" + if self.chunked: + return self._read1_chunked(n) + if self.length is not None and (n < 0 or n > self.length): + n = self.length + try: + result = self.fp.read1(n) + except ValueError: + if n >= 0: + raise + # some implementations, like BufferedReader, don't support -1 + # Read an arbitrarily selected largeish chunk. + result = self.fp.read1(16*1024) + if not result and n: + self._close_conn() + elif self.length is not None: + self.length -= len(result) + return result + + def peek(self, n=-1): + # Having this enables IOBase.readline() to read more than one + # byte at a time + if self.fp is None or self._method == "HEAD": + return b"" + if self.chunked: + return self._peek_chunked(n) + return self.fp.peek(n) + + def readline(self, limit=-1): + if self.fp is None or self._method == "HEAD": + return b"" + if self.chunked: + # Fallback to IOBase readline which uses peek() and read() + return super().readline(limit) + if self.length is not None and (limit < 0 or limit > self.length): + limit = self.length + result = self.fp.readline(limit) + if not result and limit: + self._close_conn() + elif self.length is not None: + self.length -= len(result) + return result + + def _read1_chunked(self, n): + # Strictly speaking, _get_chunk_left() may cause more than one read, + # but that is ok, since that is to satisfy the chunked protocol. + chunk_left = self._get_chunk_left() + if chunk_left is None or n == 0: + return b'' + if not (0 <= n <= chunk_left): + n = chunk_left # if n is negative or larger than chunk_left + read = self.fp.read1(n) + self.chunk_left -= len(read) + if not read: + raise IncompleteRead(b"") + return read + + def _peek_chunked(self, n): + # Strictly speaking, _get_chunk_left() may cause more than one read, + # but that is ok, since that is to satisfy the chunked protocol. + try: + chunk_left = self._get_chunk_left() + except IncompleteRead: + return b'' # peek doesn't worry about protocol + if chunk_left is None: + return b'' # eof + # peek is allowed to return more than requested. Just request the + # entire chunk, and truncate what we get. + return self.fp.peek(chunk_left)[:chunk_left] + + def fileno(self): + return self.fp.fileno() + + def getheader(self, name, default=None): + '''Returns the value of the header matching *name*. + + If there are multiple matching headers, the values are + combined into a single string separated by commas and spaces. + + If no matching header is found, returns *default* or None if + the *default* is not specified. + + If the headers are unknown, raises http.client.ResponseNotReady. + + ''' + if self.headers is None: + raise ResponseNotReady() + headers = self.headers.get_all(name) or default + if isinstance(headers, str) or not hasattr(headers, '__iter__'): + return headers + else: + return ', '.join(headers) + + def getheaders(self): + """Return list of (header, value) tuples.""" + if self.headers is None: + raise ResponseNotReady() + return list(self.headers.items()) + + # We override IOBase.__iter__ so that it doesn't check for closed-ness + + def __iter__(self): + return self + + # For compatibility with old-style urllib responses. + + def info(self): + '''Returns an instance of the class mimetools.Message containing + meta-information associated with the URL. + + When the method is HTTP, these headers are those returned by + the server at the head of the retrieved HTML page (including + Content-Length and Content-Type). + + When the method is FTP, a Content-Length header will be + present if (as is now usual) the server passed back a file + length in response to the FTP retrieval request. A + Content-Type header will be present if the MIME type can be + guessed. + + When the method is local-file, returned headers will include + a Date representing the file's last-modified time, a + Content-Length giving file size, and a Content-Type + containing a guess at the file's type. See also the + description of the mimetools module. + + ''' + return self.headers + + def geturl(self): + '''Return the real URL of the page. + + In some cases, the HTTP server redirects a client to another + URL. The urlopen() function handles this transparently, but in + some cases the caller needs to know which URL the client was + redirected to. The geturl() method can be used to get at this + redirected URL. + + ''' + return self.url + + def getcode(self): + '''Return the HTTP status code that was sent with the response, + or None if the URL is not an HTTP URL. + + ''' + return self.status + +class HTTPConnection: + + _http_vsn = 11 + _http_vsn_str = 'HTTP/1.1' + + response_class = HTTPResponse + default_port = HTTP_PORT + auto_open = 1 + debuglevel = 0 + + @staticmethod + def _is_textIO(stream): + """Test whether a file-like object is a text or a binary stream. + """ + return isinstance(stream, io.TextIOBase) + + @staticmethod + def _get_content_length(body, method): + """Get the content-length based on the body. + + If the body is None, we set Content-Length: 0 for methods that expect + a body (RFC 7230, Section 3.3.2). We also set the Content-Length for + any method if the body is a str or bytes-like object and not a file. + """ + if body is None: + # do an explicit check for not None here to distinguish + # between unset and set but empty + if method.upper() in _METHODS_EXPECTING_BODY: + return 0 + else: + return None + + if hasattr(body, 'read'): + # file-like object. + return None + + try: + # does it implement the buffer protocol (bytes, bytearray, array)? + mv = memoryview(body) + return mv.nbytes + except TypeError: + pass + + if isinstance(body, str): + return len(body) + + return None + + def __init__(self, host, port=None, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None): + self.timeout = timeout + self.source_address = source_address + self.sock = None + self._buffer = [] + self.__response = None + self.__state = _CS_IDLE + self._method = None + self._tunnel_host = None + self._tunnel_port = None + self._tunnel_headers = {} + + (self.host, self.port) = self._get_hostport(host, port) + + # This is stored as an instance variable to allow unit + # tests to replace it with a suitable mockup + self._create_connection = socket.create_connection + + def set_tunnel(self, host, port=None, headers=None): + """Set up host and port for HTTP CONNECT tunnelling. + + In a connection that uses HTTP CONNECT tunneling, the host passed to the + constructor is used as a proxy server that relays all communication to + the endpoint passed to `set_tunnel`. This done by sending an HTTP + CONNECT request to the proxy server when the connection is established. + + This method must be called before the HTML connection has been + established. + + The headers argument should be a mapping of extra HTTP headers to send + with the CONNECT request. + """ + + if self.sock: + raise RuntimeError("Can't set up tunnel for established connection") + + self._tunnel_host, self._tunnel_port = self._get_hostport(host, port) + if headers: + self._tunnel_headers = headers + else: + self._tunnel_headers.clear() + + def _get_hostport(self, host, port): + if port is None: + i = host.rfind(':') + j = host.rfind(']') # ipv6 addresses have [...] + if i > j: + try: + port = int(host[i+1:]) + except ValueError: + if host[i+1:] == "": # http://foo.com:/ == http://foo.com/ + port = self.default_port + else: + raise InvalidURL("nonnumeric port: '%s'" % host[i+1:]) + host = host[:i] + else: + port = self.default_port + if host and host[0] == '[' and host[-1] == ']': + host = host[1:-1] + + return (host, port) + + def set_debuglevel(self, level): + self.debuglevel = level + + def _tunnel(self): + connect_str = "CONNECT %s:%d HTTP/1.0\r\n" % (self._tunnel_host, + self._tunnel_port) + connect_bytes = connect_str.encode("ascii") + self.send(connect_bytes) + for header, value in self._tunnel_headers.items(): + header_str = "%s: %s\r\n" % (header, value) + header_bytes = header_str.encode("latin-1") + self.send(header_bytes) + self.send(b'\r\n') + + response = self.response_class(self.sock, method=self._method) + (version, code, message) = response._read_status() + + if code != http.HTTPStatus.OK: + self.close() + raise OSError("Tunnel connection failed: %d %s" % (code, + message.strip())) + while True: + line = response.fp.readline(_MAXLINE + 1) + if len(line) > _MAXLINE: + raise LineTooLong("header line") + if not line: + # for sites which EOF without sending a trailer + break + if line in (b'\r\n', b'\n', b''): + break + + if self.debuglevel > 0: + print('header:', line.decode()) + + def connect(self): + """Connect to the host and port specified in __init__.""" + self.sock = self._create_connection( + (self.host,self.port), self.timeout, self.source_address) + self.sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + + if self._tunnel_host: + self._tunnel() + + def close(self): + """Close the connection to the HTTP server.""" + self.__state = _CS_IDLE + try: + sock = self.sock + if sock: + self.sock = None + sock.close() # close it manually... there may be other refs + finally: + response = self.__response + if response: + self.__response = None + response.close() + + def send(self, data): + """Send `data' to the server. + ``data`` can be a string object, a bytes object, an array object, a + file-like object that supports a .read() method, or an iterable object. + """ + + if self.sock is None: + if self.auto_open: + self.connect() + else: + raise NotConnected() + + if self.debuglevel > 0: + print("send:", repr(data)) + blocksize = 8192 + if hasattr(data, "read") : + if self.debuglevel > 0: + print("sendIng a read()able") + encode = False + try: + mode = data.mode + except AttributeError: + # io.BytesIO and other file-like objects don't have a `mode` + # attribute. + pass + else: + if "b" not in mode: + encode = True + if self.debuglevel > 0: + print("encoding file using iso-8859-1") + while 1: + datablock = data.read(blocksize) + if not datablock: + break + if encode: + datablock = datablock.encode("iso-8859-1") + self.sock.sendall(datablock) + return + try: + self.sock.sendall(data) + except TypeError: + if isinstance(data, Iterable): + for d in data: + self.sock.sendall(d) + else: + raise TypeError("data should be a bytes-like object " + "or an iterable, got %r" % type(data)) + + def _output(self, s): + """Add a line of output to the current request buffer. + + Assumes that the line does *not* end with \\r\\n. + """ + self._buffer.append(s) + + def _read_readable(self, readable): + blocksize = 8192 + if self.debuglevel > 0: + print("sendIng a read()able") + encode = self._is_textIO(readable) + if encode and self.debuglevel > 0: + print("encoding file using iso-8859-1") + while True: + datablock = readable.read(blocksize) + if not datablock: + break + if encode: + datablock = datablock.encode("iso-8859-1") + yield datablock + + def _send_output(self, message_body=None, encode_chunked=False): + """Send the currently buffered request and clear the buffer. + + Appends an extra \\r\\n to the buffer. + A message_body may be specified, to be appended to the request. + """ + self._buffer.extend((b"", b"")) + msg = b"\r\n".join(self._buffer) + del self._buffer[:] + self.send(msg) + + if message_body is not None: + + # create a consistent interface to message_body + if hasattr(message_body, 'read'): + # Let file-like take precedence over byte-like. This + # is needed to allow the current position of mmap'ed + # files to be taken into account. + chunks = self._read_readable(message_body) + else: + try: + # this is solely to check to see if message_body + # implements the buffer API. it /would/ be easier + # to capture if PyObject_CheckBuffer was exposed + # to Python. + memoryview(message_body) + except TypeError: + try: + chunks = iter(message_body) + except TypeError: + raise TypeError("message_body should be a bytes-like " + "object or an iterable, got %r" + % type(message_body)) + else: + # the object implements the buffer interface and + # can be passed directly into socket methods + chunks = (message_body,) + + for chunk in chunks: + if not chunk: + if self.debuglevel > 0: + print('Zero length chunk ignored') + continue + + if encode_chunked and self._http_vsn == 11: + # chunked encoding + chunk = '{:X}\r\n'.format(len(chunk)).encode('ascii') + chunk + b'\r\n' + self.send(chunk) + + if encode_chunked and self._http_vsn == 11: + # end chunked transfer + self.send(b'0\r\n\r\n') + + def putrequest(self, method, url, skip_host=0, skip_accept_encoding=0): + """Send a request to the server. + + `method' specifies an HTTP request method, e.g. 'GET'. + `url' specifies the object being requested, e.g. '/index.html'. + `skip_host' if True does not add automatically a 'Host:' header + `skip_accept_encoding' if True does not add automatically an + 'Accept-Encoding:' header + """ + + # if a prior response has been completed, then forget about it. + if self.__response and self.__response.isclosed(): + self.__response = None + + + # in certain cases, we cannot issue another request on this connection. + # this occurs when: + # 1) we are in the process of sending a request. (_CS_REQ_STARTED) + # 2) a response to a previous request has signalled that it is going + # to close the connection upon completion. + # 3) the headers for the previous response have not been read, thus + # we cannot determine whether point (2) is true. (_CS_REQ_SENT) + # + # if there is no prior response, then we can request at will. + # + # if point (2) is true, then we will have passed the socket to the + # response (effectively meaning, "there is no prior response"), and + # will open a new one when a new request is made. + # + # Note: if a prior response exists, then we *can* start a new request. + # We are not allowed to begin fetching the response to this new + # request, however, until that prior response is complete. + # + if self.__state == _CS_IDLE: + self.__state = _CS_REQ_STARTED + else: + raise CannotSendRequest(self.__state) + + # Save the method we use, we need it later in the response phase + self._method = method + if not url: + url = '/' + request = '%s %s %s' % (method, url, self._http_vsn_str) + + # Non-ASCII characters should have been eliminated earlier + self._output(request.encode('ascii')) + + if self._http_vsn == 11: + # Issue some standard headers for better HTTP/1.1 compliance + + if not skip_host: + # this header is issued *only* for HTTP/1.1 + # connections. more specifically, this means it is + # only issued when the client uses the new + # HTTPConnection() class. backwards-compat clients + # will be using HTTP/1.0 and those clients may be + # issuing this header themselves. we should NOT issue + # it twice; some web servers (such as Apache) barf + # when they see two Host: headers + + # If we need a non-standard port,include it in the + # header. If the request is going through a proxy, + # but the host of the actual URL, not the host of the + # proxy. + + netloc = '' + if url.startswith('http'): + nil, netloc, nil, nil, nil = urlsplit(url) + + if netloc: + try: + netloc_enc = netloc.encode("ascii") + except UnicodeEncodeError: + netloc_enc = netloc.encode("idna") + self.putheader('Host', netloc_enc) + else: + if self._tunnel_host: + host = self._tunnel_host + port = self._tunnel_port + else: + host = self.host + port = self.port + + try: + host_enc = host.encode("ascii") + except UnicodeEncodeError: + host_enc = host.encode("idna") + + # As per RFC 273, IPv6 address should be wrapped with [] + # when used as Host header + + if host.find(':') >= 0: + host_enc = b'[' + host_enc + b']' + + if port == self.default_port: + self.putheader('Host', host_enc) + else: + host_enc = host_enc.decode("ascii") + self.putheader('Host', "%s:%s" % (host_enc, port)) + + # note: we are assuming that clients will not attempt to set these + # headers since *this* library must deal with the + # consequences. this also means that when the supporting + # libraries are updated to recognize other forms, then this + # code should be changed (removed or updated). + + # we only want a Content-Encoding of "identity" since we don't + # support encodings such as x-gzip or x-deflate. + if not skip_accept_encoding: + self.putheader('Accept-Encoding', 'identity') + + # we can accept "chunked" Transfer-Encodings, but no others + # NOTE: no TE header implies *only* "chunked" + #self.putheader('TE', 'chunked') + + # if TE is supplied in the header, then it must appear in a + # Connection header. + #self.putheader('Connection', 'TE') + + else: + # For HTTP/1.0, the server will assume "not chunked" + pass + + def putheader(self, header, *values): + """Send a request header line to the server. + + For example: h.putheader('Accept', 'text/html') + """ + if self.__state != _CS_REQ_STARTED: + raise CannotSendHeader() + + if hasattr(header, 'encode'): + header = header.encode('ascii') + + if not _is_legal_header_name(header): + raise ValueError('Invalid header name %r' % (header,)) + + values = list(values) + for i, one_value in enumerate(values): + if hasattr(one_value, 'encode'): + values[i] = one_value.encode('latin-1') + elif isinstance(one_value, int): + values[i] = str(one_value).encode('ascii') + + if _is_illegal_header_value(values[i]): + raise ValueError('Invalid header value %r' % (values[i],)) + + value = b'\r\n\t'.join(values) + header = header + b': ' + value + self._output(header) + + def endheaders(self, message_body=None, **kwds): + """Indicate that the last header line has been sent to the server. + + This method sends the request to the server. The optional message_body + argument can be used to pass a message body associated with the + request. + """ + encode_chunked = kwds.pop('encode_chunked', False) + if kwds: + # mimic interpreter error for unrecognized keyword + raise TypeError("endheaders() got an unexpected keyword argument '{}'" + .format(kwds.popitem()[0])) + + if self.__state == _CS_REQ_STARTED: + self.__state = _CS_REQ_SENT + else: + raise CannotSendHeader() + self._send_output(message_body, encode_chunked=encode_chunked) + + def request(self, method, url, body=None, headers={}, **kwds): + """Send a complete request to the server.""" + encode_chunked = kwds.pop('encode_chunked', False) + if kwds: + # mimic interpreter error for unrecognized keyword + raise TypeError("request() got an unexpected keyword argument '{}'" + .format(kwds.popitem()[0])) + self._send_request(method, url, body, headers, encode_chunked) + + def _set_content_length(self, body, method): + # Set the content-length based on the body. If the body is "empty", we + # set Content-Length: 0 for methods that expect a body (RFC 7230, + # Section 3.3.2). If the body is set for other methods, we set the + # header provided we can figure out what the length is. + thelen = None + method_expects_body = method.upper() in _METHODS_EXPECTING_BODY + if body is None and method_expects_body: + thelen = '0' + elif body is not None: + try: + thelen = str(len(body)) + except TypeError: + # If this is a file-like object, try to + # fstat its file descriptor + try: + thelen = str(os.fstat(body.fileno()).st_size) + except (AttributeError, OSError): + # Don't send a length if this failed + if self.debuglevel > 0: print("Cannot stat!!") + + if thelen is not None: + self.putheader('Content-Length', thelen) + + def _send_request(self, method, url, body, headers, encode_chunked): + # Honor explicitly requested Host: and Accept-Encoding: headers. + header_names = frozenset(k.lower() for k in headers) + skips = {} + if 'host' in header_names: + skips['skip_host'] = 1 + if 'accept-encoding' in header_names: + skips['skip_accept_encoding'] = 1 + + self.putrequest(method, url, **skips) + + # chunked encoding will happen if HTTP/1.1 is used and either + # the caller passes encode_chunked=True or the following + # conditions hold: + # 1. content-length has not been explicitly set + # 2. the body is a file or iterable, but not a str or bytes-like + # 3. Transfer-Encoding has NOT been explicitly set by the caller + + if 'content-length' not in header_names: + # only chunk body if not explicitly set for backwards + # compatibility, assuming the client code is already handling the + # chunking + if 'transfer-encoding' not in header_names: + # if content-length cannot be automatically determined, fall + # back to chunked encoding + encode_chunked = False + content_length = self._get_content_length(body, method) + if content_length is None: + if body is not None: + if self.debuglevel > 0: + print('Unable to determine size of %r' % body) + encode_chunked = True + self.putheader('Transfer-Encoding', 'chunked') + else: + self.putheader('Content-Length', str(content_length)) + else: + encode_chunked = False + + for hdr, value in headers.items(): + self.putheader(hdr, value) + if isinstance(body, str): + # RFC 2616 Section 3.7.1 says that text default has a + # default charset of iso-8859-1. + body = _encode(body, 'body') + self.endheaders(body, encode_chunked=encode_chunked) + + def getresponse(self): + """Get the response from the server. + + If the HTTPConnection is in the correct state, returns an + instance of HTTPResponse or of whatever object is returned by + the response_class variable. + + If a request has not been sent or if a previous response has + not be handled, ResponseNotReady is raised. If the HTTP + response indicates that the connection should be closed, then + it will be closed before the response is returned. When the + connection is closed, the underlying socket is closed. + """ + + # if a prior response has been completed, then forget about it. + if self.__response and self.__response.isclosed(): + self.__response = None + + # if a prior response exists, then it must be completed (otherwise, we + # cannot read this response's header to determine the connection-close + # behavior) + # + # note: if a prior response existed, but was connection-close, then the + # socket and response were made independent of this HTTPConnection + # object since a new request requires that we open a whole new + # connection + # + # this means the prior response had one of two states: + # 1) will_close: this connection was reset and the prior socket and + # response operate independently + # 2) persistent: the response was retained and we await its + # isclosed() status to become true. + # + if self.__state != _CS_REQ_SENT or self.__response: + raise ResponseNotReady(self.__state) + + if self.debuglevel > 0: + response = self.response_class(self.sock, self.debuglevel, + method=self._method) + else: + response = self.response_class(self.sock, method=self._method) + + try: + try: + response.begin() + except ConnectionError: + self.close() + raise + assert response.will_close != _UNKNOWN + self.__state = _CS_IDLE + + if response.will_close: + # this effectively passes the connection to the response + self.close() + else: + # remember this, so we can tell when it is complete + self.__response = response + + return response + except: + response.close() + raise + +try: + from eventlet.green import ssl +except ImportError: + pass +else: + def _create_https_context(http_version): + # Function also used by urllib.request to be able to set the check_hostname + # attribute on a context object. + context = ssl._create_default_https_context() + # send ALPN extension to indicate HTTP/1.1 protocol + if http_version == 11: + context.set_alpn_protocols(['http/1.1']) + # enable PHA for TLS 1.3 connections if available + if context.post_handshake_auth is not None: + context.post_handshake_auth = True + return context + + def _populate_https_context(context, check_hostname): + if check_hostname is not None: + context.check_hostname = check_hostname + + class HTTPSConnection(HTTPConnection): + "This class allows communication via SSL." + + default_port = HTTPS_PORT + + # XXX Should key_file and cert_file be deprecated in favour of context? + + def __init__(self, host, port=None, key_file=None, cert_file=None, + timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None, *, context=None, + check_hostname=None): + super().__init__(host, port, timeout, + source_address) + self.key_file = key_file + self.cert_file = cert_file + if context is None: + context = _create_https_context(self._http_vsn) + _populate_https_context(context, check_hostname) + if key_file or cert_file: + context.load_cert_chain(cert_file, key_file) + self._context = context + self._check_hostname = check_hostname + + def connect(self): + "Connect to a host on a given (SSL) port." + + super().connect() + + if self._tunnel_host: + server_hostname = self._tunnel_host + else: + server_hostname = self.host + + self.sock = self._context.wrap_socket(self.sock, + server_hostname=server_hostname) + if not self._context.check_hostname and self._check_hostname: + try: + ssl.match_hostname(self.sock.getpeercert(), server_hostname) + except Exception: + self.sock.shutdown(socket.SHUT_RDWR) + self.sock.close() + raise + + __all__.append("HTTPSConnection") + +class HTTPException(Exception): + # Subclasses that define an __init__ must call Exception.__init__ + # or define self.args. Otherwise, str() will fail. + pass + +class NotConnected(HTTPException): + pass + +class InvalidURL(HTTPException): + pass + +class UnknownProtocol(HTTPException): + def __init__(self, version): + self.args = version, + self.version = version + +class UnknownTransferEncoding(HTTPException): + pass + +class UnimplementedFileMode(HTTPException): + pass + +class IncompleteRead(HTTPException): + def __init__(self, partial, expected=None): + self.args = partial, + self.partial = partial + self.expected = expected + def __repr__(self): + if self.expected is not None: + e = ', %i more expected' % self.expected + else: + e = '' + return '%s(%i bytes read%s)' % (self.__class__.__name__, + len(self.partial), e) + def __str__(self): + return repr(self) + +class ImproperConnectionState(HTTPException): + pass + +class CannotSendRequest(ImproperConnectionState): + pass + +class CannotSendHeader(ImproperConnectionState): + pass + +class ResponseNotReady(ImproperConnectionState): + pass + +class BadStatusLine(HTTPException): + def __init__(self, line): + if not line: + line = repr(line) + self.args = line, + self.line = line + +class LineTooLong(HTTPException): + def __init__(self, line_type): + HTTPException.__init__(self, "got more than %d bytes when reading %s" + % (_MAXLINE, line_type)) + +class RemoteDisconnected(ConnectionResetError, BadStatusLine): + def __init__(self, *pos, **kw): + BadStatusLine.__init__(self, "") + ConnectionResetError.__init__(self, *pos, **kw) + +# for backwards compatibility +error = HTTPException diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/http/cookiejar.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/http/cookiejar.py new file mode 100644 index 0000000..0394ca5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/http/cookiejar.py @@ -0,0 +1,2154 @@ +# This is part of Python source code with Eventlet-specific modifications. +# +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights +# Reserved +# +# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +# -------------------------------------------- +# +# 1. This LICENSE AGREEMENT is between the Python Software Foundation +# ("PSF"), and the Individual or Organization ("Licensee") accessing and +# otherwise using this software ("Python") in source or binary form and +# its associated documentation. +# +# 2. Subject to the terms and conditions of this License Agreement, PSF hereby +# grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +# analyze, test, perform and/or display publicly, prepare derivative works, +# distribute, and otherwise use Python alone or in any derivative version, +# provided, however, that PSF's License Agreement and PSF's notice of copyright, +# i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights +# Reserved" are retained in Python alone or in any derivative version prepared by +# Licensee. +# +# 3. In the event Licensee prepares a derivative work that is based on +# or incorporates Python or any part thereof, and wants to make +# the derivative work available to others as provided herein, then +# Licensee hereby agrees to include in any such work a brief summary of +# the changes made to Python. +# +# 4. PSF is making Python available to Licensee on an "AS IS" +# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +# INFRINGE ANY THIRD PARTY RIGHTS. +# +# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. +# +# 6. This License Agreement will automatically terminate upon a material +# breach of its terms and conditions. +# +# 7. Nothing in this License Agreement shall be deemed to create any +# relationship of agency, partnership, or joint venture between PSF and +# Licensee. This License Agreement does not grant permission to use PSF +# trademarks or trade name in a trademark sense to endorse or promote +# products or services of Licensee, or any third party. +# +# 8. By copying, installing or otherwise using Python, Licensee +# agrees to be bound by the terms and conditions of this License +# Agreement. +r"""HTTP cookie handling for web clients. + +This module has (now fairly distant) origins in Gisle Aas' Perl module +HTTP::Cookies, from the libwww-perl library. + +Docstrings, comments and debug strings in this code refer to the +attributes of the HTTP cookie system as cookie-attributes, to distinguish +them clearly from Python attributes. + +Class diagram (note that BSDDBCookieJar and the MSIE* classes are not +distributed with the Python standard library, but are available from +http://wwwsearch.sf.net/): + + CookieJar____ + / \ \ + FileCookieJar \ \ + / | \ \ \ + MozillaCookieJar | LWPCookieJar \ \ + | | \ + | ---MSIEBase | \ + | / | | \ + | / MSIEDBCookieJar BSDDBCookieJar + |/ + MSIECookieJar + +""" + +__all__ = ['Cookie', 'CookieJar', 'CookiePolicy', 'DefaultCookiePolicy', + 'FileCookieJar', 'LWPCookieJar', 'LoadError', 'MozillaCookieJar'] + +import copy +import datetime +import re +import time +# Eventlet change: urllib.request used to be imported here but it's not used, +# removed for clarity +import urllib.parse +from calendar import timegm + +from eventlet.green import threading as _threading, time +from eventlet.green.http import client as http_client # only for the default HTTP port + +debug = False # set to True to enable debugging via the logging module +logger = None + +def _debug(*args): + if not debug: + return + global logger + if not logger: + import logging + logger = logging.getLogger("http.cookiejar") + return logger.debug(*args) + + +DEFAULT_HTTP_PORT = str(http_client.HTTP_PORT) +MISSING_FILENAME_TEXT = ("a filename was not supplied (nor was the CookieJar " + "instance initialised with one)") + +def _warn_unhandled_exception(): + # There are a few catch-all except: statements in this module, for + # catching input that's bad in unexpected ways. Warn if any + # exceptions are caught there. + import io, warnings, traceback + f = io.StringIO() + traceback.print_exc(None, f) + msg = f.getvalue() + warnings.warn("http.cookiejar bug!\n%s" % msg, stacklevel=2) + + +# Date/time conversion +# ----------------------------------------------------------------------------- + +EPOCH_YEAR = 1970 +def _timegm(tt): + year, month, mday, hour, min, sec = tt[:6] + if ((year >= EPOCH_YEAR) and (1 <= month <= 12) and (1 <= mday <= 31) and + (0 <= hour <= 24) and (0 <= min <= 59) and (0 <= sec <= 61)): + return timegm(tt) + else: + return None + +DAYS = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] +MONTHS = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] +MONTHS_LOWER = [] +for month in MONTHS: MONTHS_LOWER.append(month.lower()) + +def time2isoz(t=None): + """Return a string representing time in seconds since epoch, t. + + If the function is called without an argument, it will use the current + time. + + The format of the returned string is like "YYYY-MM-DD hh:mm:ssZ", + representing Universal Time (UTC, aka GMT). An example of this format is: + + 1994-11-24 08:49:37Z + + """ + if t is None: + dt = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + else: + dt = datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc + ).replace(tzinfo=None) + return "%04d-%02d-%02d %02d:%02d:%02dZ" % ( + dt.year, dt.month, dt.day, dt.hour, dt.minute, dt.second) + +def time2netscape(t=None): + """Return a string representing time in seconds since epoch, t. + + If the function is called without an argument, it will use the current + time. + + The format of the returned string is like this: + + Wed, DD-Mon-YYYY HH:MM:SS GMT + + """ + if t is None: + dt = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + else: + dt = datetime.datetime.fromtimestamp(t, tz=datetime.timezone.utc + ).replace(tzinfo=None) + return "%s %02d-%s-%04d %02d:%02d:%02d GMT" % ( + DAYS[dt.weekday()], dt.day, MONTHS[dt.month-1], + dt.year, dt.hour, dt.minute, dt.second) + + +UTC_ZONES = {"GMT": None, "UTC": None, "UT": None, "Z": None} + +TIMEZONE_RE = re.compile(r"^([-+])?(\d\d?):?(\d\d)?$", re.ASCII) +def offset_from_tz_string(tz): + offset = None + if tz in UTC_ZONES: + offset = 0 + else: + m = TIMEZONE_RE.search(tz) + if m: + offset = 3600 * int(m.group(2)) + if m.group(3): + offset = offset + 60 * int(m.group(3)) + if m.group(1) == '-': + offset = -offset + return offset + +def _str2time(day, mon, yr, hr, min, sec, tz): + yr = int(yr) + if yr > datetime.MAXYEAR: + return None + + # translate month name to number + # month numbers start with 1 (January) + try: + mon = MONTHS_LOWER.index(mon.lower())+1 + except ValueError: + # maybe it's already a number + try: + imon = int(mon) + except ValueError: + return None + if 1 <= imon <= 12: + mon = imon + else: + return None + + # make sure clock elements are defined + if hr is None: hr = 0 + if min is None: min = 0 + if sec is None: sec = 0 + + day = int(day) + hr = int(hr) + min = int(min) + sec = int(sec) + + if yr < 1000: + # find "obvious" year + cur_yr = time.localtime(time.time())[0] + m = cur_yr % 100 + tmp = yr + yr = yr + cur_yr - m + m = m - tmp + if abs(m) > 50: + if m > 0: yr = yr + 100 + else: yr = yr - 100 + + # convert UTC time tuple to seconds since epoch (not timezone-adjusted) + t = _timegm((yr, mon, day, hr, min, sec, tz)) + + if t is not None: + # adjust time using timezone string, to get absolute time since epoch + if tz is None: + tz = "UTC" + tz = tz.upper() + offset = offset_from_tz_string(tz) + if offset is None: + return None + t = t - offset + + return t + +STRICT_DATE_RE = re.compile( + r"^[SMTWF][a-z][a-z], (\d\d) ([JFMASOND][a-z][a-z]) " + r"(\d\d\d\d) (\d\d):(\d\d):(\d\d) GMT$", re.ASCII) +WEEKDAY_RE = re.compile( + r"^(?:Sun|Mon|Tue|Wed|Thu|Fri|Sat)[a-z]*,?\s*", re.I | re.ASCII) +LOOSE_HTTP_DATE_RE = re.compile( + r"""^ + (\d\d?) # day + (?:\s+|[-\/]) + (\w+) # month + (?:\s+|[-\/]) + (\d+) # year + (?: + (?:\s+|:) # separator before clock + (\d\d?):(\d\d) # hour:min + (?::(\d\d))? # optional seconds + )? # optional clock + \s* + ([-+]?\d{2,4}|(?![APap][Mm]\b)[A-Za-z]+)? # timezone + \s* + (?:\(\w+\))? # ASCII representation of timezone in parens. + \s*$""", re.X | re.ASCII) +def http2time(text): + """Returns time in seconds since epoch of time represented by a string. + + Return value is an integer. + + None is returned if the format of str is unrecognized, the time is outside + the representable range, or the timezone string is not recognized. If the + string contains no timezone, UTC is assumed. + + The timezone in the string may be numerical (like "-0800" or "+0100") or a + string timezone (like "UTC", "GMT", "BST" or "EST"). Currently, only the + timezone strings equivalent to UTC (zero offset) are known to the function. + + The function loosely parses the following formats: + + Wed, 09 Feb 1994 22:23:32 GMT -- HTTP format + Tuesday, 08-Feb-94 14:15:29 GMT -- old rfc850 HTTP format + Tuesday, 08-Feb-1994 14:15:29 GMT -- broken rfc850 HTTP format + 09 Feb 1994 22:23:32 GMT -- HTTP format (no weekday) + 08-Feb-94 14:15:29 GMT -- rfc850 format (no weekday) + 08-Feb-1994 14:15:29 GMT -- broken rfc850 format (no weekday) + + The parser ignores leading and trailing whitespace. The time may be + absent. + + If the year is given with only 2 digits, the function will select the + century that makes the year closest to the current date. + + """ + # fast exit for strictly conforming string + m = STRICT_DATE_RE.search(text) + if m: + g = m.groups() + mon = MONTHS_LOWER.index(g[1].lower()) + 1 + tt = (int(g[2]), mon, int(g[0]), + int(g[3]), int(g[4]), float(g[5])) + return _timegm(tt) + + # No, we need some messy parsing... + + # clean up + text = text.lstrip() + text = WEEKDAY_RE.sub("", text, 1) # Useless weekday + + # tz is time zone specifier string + day, mon, yr, hr, min, sec, tz = [None]*7 + + # loose regexp parse + m = LOOSE_HTTP_DATE_RE.search(text) + if m is not None: + day, mon, yr, hr, min, sec, tz = m.groups() + else: + return None # bad format + + return _str2time(day, mon, yr, hr, min, sec, tz) + +ISO_DATE_RE = re.compile( + r"""^ + (\d{4}) # year + [-\/]? + (\d\d?) # numerical month + [-\/]? + (\d\d?) # day + (?: + (?:\s+|[-:Tt]) # separator before clock + (\d\d?):?(\d\d) # hour:min + (?::?(\d\d(?:\.\d*)?))? # optional seconds (and fractional) + )? # optional clock + \s* + ([-+]?\d\d?:?(:?\d\d)? + |Z|z)? # timezone (Z is "zero meridian", i.e. GMT) + \s*$""", re.X | re. ASCII) +def iso2time(text): + """ + As for http2time, but parses the ISO 8601 formats: + + 1994-02-03 14:15:29 -0100 -- ISO 8601 format + 1994-02-03 14:15:29 -- zone is optional + 1994-02-03 -- only date + 1994-02-03T14:15:29 -- Use T as separator + 19940203T141529Z -- ISO 8601 compact format + 19940203 -- only date + + """ + # clean up + text = text.lstrip() + + # tz is time zone specifier string + day, mon, yr, hr, min, sec, tz = [None]*7 + + # loose regexp parse + m = ISO_DATE_RE.search(text) + if m is not None: + # XXX there's an extra bit of the timezone I'm ignoring here: is + # this the right thing to do? + yr, mon, day, hr, min, sec, tz, _ = m.groups() + else: + return None # bad format + + return _str2time(day, mon, yr, hr, min, sec, tz) + + +# Header parsing +# ----------------------------------------------------------------------------- + +def unmatched(match): + """Return unmatched part of re.Match object.""" + start, end = match.span(0) + return match.string[:start]+match.string[end:] + +HEADER_TOKEN_RE = re.compile(r"^\s*([^=\s;,]+)") +HEADER_QUOTED_VALUE_RE = re.compile(r"^\s*=\s*\"([^\"\\]*(?:\\.[^\"\\]*)*)\"") +HEADER_VALUE_RE = re.compile(r"^\s*=\s*([^\s;,]*)") +HEADER_ESCAPE_RE = re.compile(r"\\(.)") +def split_header_words(header_values): + r"""Parse header values into a list of lists containing key,value pairs. + + The function knows how to deal with ",", ";" and "=" as well as quoted + values after "=". A list of space separated tokens are parsed as if they + were separated by ";". + + If the header_values passed as argument contains multiple values, then they + are treated as if they were a single value separated by comma ",". + + This means that this function is useful for parsing header fields that + follow this syntax (BNF as from the HTTP/1.1 specification, but we relax + the requirement for tokens). + + headers = #header + header = (token | parameter) *( [";"] (token | parameter)) + + token = 1* + separators = "(" | ")" | "<" | ">" | "@" + | "," | ";" | ":" | "\" | <"> + | "/" | "[" | "]" | "?" | "=" + | "{" | "}" | SP | HT + + quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) + qdtext = > + quoted-pair = "\" CHAR + + parameter = attribute "=" value + attribute = token + value = token | quoted-string + + Each header is represented by a list of key/value pairs. The value for a + simple token (not part of a parameter) is None. Syntactically incorrect + headers will not necessarily be parsed as you would want. + + This is easier to describe with some examples: + + >>> split_header_words(['foo="bar"; port="80,81"; discard, bar=baz']) + [[('foo', 'bar'), ('port', '80,81'), ('discard', None)], [('bar', 'baz')]] + >>> split_header_words(['text/html; charset="iso-8859-1"']) + [[('text/html', None), ('charset', 'iso-8859-1')]] + >>> split_header_words([r'Basic realm="\"foo\bar\""']) + [[('Basic', None), ('realm', '"foobar"')]] + + """ + assert not isinstance(header_values, str) + result = [] + for text in header_values: + orig_text = text + pairs = [] + while text: + m = HEADER_TOKEN_RE.search(text) + if m: + text = unmatched(m) + name = m.group(1) + m = HEADER_QUOTED_VALUE_RE.search(text) + if m: # quoted value + text = unmatched(m) + value = m.group(1) + value = HEADER_ESCAPE_RE.sub(r"\1", value) + else: + m = HEADER_VALUE_RE.search(text) + if m: # unquoted value + text = unmatched(m) + value = m.group(1) + value = value.rstrip() + else: + # no value, a lone token + value = None + pairs.append((name, value)) + elif text.lstrip().startswith(","): + # concatenated headers, as per RFC 2616 section 4.2 + text = text.lstrip()[1:] + if pairs: result.append(pairs) + pairs = [] + else: + # skip junk + non_junk, nr_junk_chars = re.subn(r"^[=\s;]*", "", text) + assert nr_junk_chars > 0, ( + "split_header_words bug: '%s', '%s', %s" % + (orig_text, text, pairs)) + text = non_junk + if pairs: result.append(pairs) + return result + +HEADER_JOIN_ESCAPE_RE = re.compile(r"([\"\\])") +def join_header_words(lists): + """Do the inverse (almost) of the conversion done by split_header_words. + + Takes a list of lists of (key, value) pairs and produces a single header + value. Attribute values are quoted if needed. + + >>> join_header_words([[("text/plain", None), ("charset", "iso-8859-1")]]) + 'text/plain; charset="iso-8859-1"' + >>> join_header_words([[("text/plain", None)], [("charset", "iso-8859-1")]]) + 'text/plain, charset="iso-8859-1"' + + """ + headers = [] + for pairs in lists: + attr = [] + for k, v in pairs: + if v is not None: + if not re.search(r"^\w+$", v): + v = HEADER_JOIN_ESCAPE_RE.sub(r"\\\1", v) # escape " and \ + v = '"%s"' % v + k = "%s=%s" % (k, v) + attr.append(k) + if attr: headers.append("; ".join(attr)) + return ", ".join(headers) + +def strip_quotes(text): + if text.startswith('"'): + text = text[1:] + if text.endswith('"'): + text = text[:-1] + return text + +def parse_ns_headers(ns_headers): + """Ad-hoc parser for Netscape protocol cookie-attributes. + + The old Netscape cookie format for Set-Cookie can for instance contain + an unquoted "," in the expires field, so we have to use this ad-hoc + parser instead of split_header_words. + + XXX This may not make the best possible effort to parse all the crap + that Netscape Cookie headers contain. Ronald Tschalar's HTTPClient + parser is probably better, so could do worse than following that if + this ever gives any trouble. + + Currently, this is also used for parsing RFC 2109 cookies. + + """ + known_attrs = ("expires", "domain", "path", "secure", + # RFC 2109 attrs (may turn up in Netscape cookies, too) + "version", "port", "max-age") + + result = [] + for ns_header in ns_headers: + pairs = [] + version_set = False + + # XXX: The following does not strictly adhere to RFCs in that empty + # names and values are legal (the former will only appear once and will + # be overwritten if multiple occurrences are present). This is + # mostly to deal with backwards compatibility. + for ii, param in enumerate(ns_header.split(';')): + param = param.strip() + + key, sep, val = param.partition('=') + key = key.strip() + + if not key: + if ii == 0: + break + else: + continue + + # allow for a distinction between present and empty and missing + # altogether + val = val.strip() if sep else None + + if ii != 0: + lc = key.lower() + if lc in known_attrs: + key = lc + + if key == "version": + # This is an RFC 2109 cookie. + if val is not None: + val = strip_quotes(val) + version_set = True + elif key == "expires": + # convert expires date to seconds since epoch + if val is not None: + val = http2time(strip_quotes(val)) # None if invalid + pairs.append((key, val)) + + if pairs: + if not version_set: + pairs.append(("version", "0")) + result.append(pairs) + + return result + + +IPV4_RE = re.compile(r"\.\d+$", re.ASCII) +def is_HDN(text): + """Return True if text is a host domain name.""" + # XXX + # This may well be wrong. Which RFC is HDN defined in, if any (for + # the purposes of RFC 2965)? + # For the current implementation, what about IPv6? Remember to look + # at other uses of IPV4_RE also, if change this. + if IPV4_RE.search(text): + return False + if text == "": + return False + if text[0] == "." or text[-1] == ".": + return False + return True + +def domain_match(A, B): + """Return True if domain A domain-matches domain B, according to RFC 2965. + + A and B may be host domain names or IP addresses. + + RFC 2965, section 1: + + Host names can be specified either as an IP address or a HDN string. + Sometimes we compare one host name with another. (Such comparisons SHALL + be case-insensitive.) Host A's name domain-matches host B's if + + * their host name strings string-compare equal; or + + * A is a HDN string and has the form NB, where N is a non-empty + name string, B has the form .B', and B' is a HDN string. (So, + x.y.com domain-matches .Y.com but not Y.com.) + + Note that domain-match is not a commutative operation: a.b.c.com + domain-matches .c.com, but not the reverse. + + """ + # Note that, if A or B are IP addresses, the only relevant part of the + # definition of the domain-match algorithm is the direct string-compare. + A = A.lower() + B = B.lower() + if A == B: + return True + if not is_HDN(A): + return False + i = A.rfind(B) + if i == -1 or i == 0: + # A does not have form NB, or N is the empty string + return False + if not B.startswith("."): + return False + if not is_HDN(B[1:]): + return False + return True + +def liberal_is_HDN(text): + """Return True if text is a sort-of-like a host domain name. + + For accepting/blocking domains. + + """ + if IPV4_RE.search(text): + return False + return True + +def user_domain_match(A, B): + """For blocking/accepting domains. + + A and B may be host domain names or IP addresses. + + """ + A = A.lower() + B = B.lower() + if not (liberal_is_HDN(A) and liberal_is_HDN(B)): + if A == B: + # equal IP addresses + return True + return False + initial_dot = B.startswith(".") + if initial_dot and A.endswith(B): + return True + if not initial_dot and A == B: + return True + return False + +cut_port_re = re.compile(r":\d+$", re.ASCII) +def request_host(request): + """Return request-host, as defined by RFC 2965. + + Variation from RFC: returned value is lowercased, for convenient + comparison. + + """ + url = request.get_full_url() + host = urllib.parse.urlparse(url)[1] + if host == "": + host = request.get_header("Host", "") + + # remove port, if present + host = cut_port_re.sub("", host, 1) + return host.lower() + +def eff_request_host(request): + """Return a tuple (request-host, effective request-host name). + + As defined by RFC 2965, except both are lowercased. + + """ + erhn = req_host = request_host(request) + if req_host.find(".") == -1 and not IPV4_RE.search(req_host): + erhn = req_host + ".local" + return req_host, erhn + +def request_path(request): + """Path component of request-URI, as defined by RFC 2965.""" + url = request.get_full_url() + parts = urllib.parse.urlsplit(url) + path = escape_path(parts.path) + if not path.startswith("/"): + # fix bad RFC 2396 absoluteURI + path = "/" + path + return path + +def request_port(request): + host = request.host + i = host.find(':') + if i >= 0: + port = host[i+1:] + try: + int(port) + except ValueError: + _debug("nonnumeric port: '%s'", port) + return None + else: + port = DEFAULT_HTTP_PORT + return port + +# Characters in addition to A-Z, a-z, 0-9, '_', '.', and '-' that don't +# need to be escaped to form a valid HTTP URL (RFCs 2396 and 1738). +HTTP_PATH_SAFE = "%/;:@&=+$,!~*'()" +ESCAPED_CHAR_RE = re.compile(r"%([0-9a-fA-F][0-9a-fA-F])") +def uppercase_escaped_char(match): + return "%%%s" % match.group(1).upper() +def escape_path(path): + """Escape any invalid characters in HTTP URL, and uppercase all escapes.""" + # There's no knowing what character encoding was used to create URLs + # containing %-escapes, but since we have to pick one to escape invalid + # path characters, we pick UTF-8, as recommended in the HTML 4.0 + # specification: + # http://www.w3.org/TR/REC-html40/appendix/notes.html#h-B.2.1 + # And here, kind of: draft-fielding-uri-rfc2396bis-03 + # (And in draft IRI specification: draft-duerst-iri-05) + # (And here, for new URI schemes: RFC 2718) + path = urllib.parse.quote(path, HTTP_PATH_SAFE) + path = ESCAPED_CHAR_RE.sub(uppercase_escaped_char, path) + return path + +def reach(h): + """Return reach of host h, as defined by RFC 2965, section 1. + + The reach R of a host name H is defined as follows: + + * If + + - H is the host domain name of a host; and, + + - H has the form A.B; and + + - A has no embedded (that is, interior) dots; and + + - B has at least one embedded dot, or B is the string "local". + then the reach of H is .B. + + * Otherwise, the reach of H is H. + + >>> reach("www.acme.com") + '.acme.com' + >>> reach("acme.com") + 'acme.com' + >>> reach("acme.local") + '.local' + + """ + i = h.find(".") + if i >= 0: + #a = h[:i] # this line is only here to show what a is + b = h[i+1:] + i = b.find(".") + if is_HDN(h) and (i >= 0 or b == "local"): + return "."+b + return h + +def is_third_party(request): + """ + + RFC 2965, section 3.3.6: + + An unverifiable transaction is to a third-party host if its request- + host U does not domain-match the reach R of the request-host O in the + origin transaction. + + """ + req_host = request_host(request) + if not domain_match(req_host, reach(request.origin_req_host)): + return True + else: + return False + + +class Cookie: + """HTTP Cookie. + + This class represents both Netscape and RFC 2965 cookies. + + This is deliberately a very simple class. It just holds attributes. It's + possible to construct Cookie instances that don't comply with the cookie + standards. CookieJar.make_cookies is the factory function for Cookie + objects -- it deals with cookie parsing, supplying defaults, and + normalising to the representation used in this class. CookiePolicy is + responsible for checking them to see whether they should be accepted from + and returned to the server. + + Note that the port may be present in the headers, but unspecified ("Port" + rather than"Port=80", for example); if this is the case, port is None. + + """ + + def __init__(self, version, name, value, + port, port_specified, + domain, domain_specified, domain_initial_dot, + path, path_specified, + secure, + expires, + discard, + comment, + comment_url, + rest, + rfc2109=False, + ): + + if version is not None: version = int(version) + if expires is not None: expires = int(float(expires)) + if port is None and port_specified is True: + raise ValueError("if port is None, port_specified must be false") + + self.version = version + self.name = name + self.value = value + self.port = port + self.port_specified = port_specified + # normalise case, as per RFC 2965 section 3.3.3 + self.domain = domain.lower() + self.domain_specified = domain_specified + # Sigh. We need to know whether the domain given in the + # cookie-attribute had an initial dot, in order to follow RFC 2965 + # (as clarified in draft errata). Needed for the returned $Domain + # value. + self.domain_initial_dot = domain_initial_dot + self.path = path + self.path_specified = path_specified + self.secure = secure + self.expires = expires + self.discard = discard + self.comment = comment + self.comment_url = comment_url + self.rfc2109 = rfc2109 + + self._rest = copy.copy(rest) + + def has_nonstandard_attr(self, name): + return name in self._rest + def get_nonstandard_attr(self, name, default=None): + return self._rest.get(name, default) + def set_nonstandard_attr(self, name, value): + self._rest[name] = value + + def is_expired(self, now=None): + if now is None: now = time.time() + if (self.expires is not None) and (self.expires <= now): + return True + return False + + def __str__(self): + if self.port is None: p = "" + else: p = ":"+self.port + limit = self.domain + p + self.path + if self.value is not None: + namevalue = "%s=%s" % (self.name, self.value) + else: + namevalue = self.name + return "" % (namevalue, limit) + + def __repr__(self): + args = [] + for name in ("version", "name", "value", + "port", "port_specified", + "domain", "domain_specified", "domain_initial_dot", + "path", "path_specified", + "secure", "expires", "discard", "comment", "comment_url", + ): + attr = getattr(self, name) + args.append("%s=%s" % (name, repr(attr))) + args.append("rest=%s" % repr(self._rest)) + args.append("rfc2109=%s" % repr(self.rfc2109)) + return "%s(%s)" % (self.__class__.__name__, ", ".join(args)) + + +class CookiePolicy: + """Defines which cookies get accepted from and returned to server. + + May also modify cookies, though this is probably a bad idea. + + The subclass DefaultCookiePolicy defines the standard rules for Netscape + and RFC 2965 cookies -- override that if you want a customised policy. + + """ + def set_ok(self, cookie, request): + """Return true if (and only if) cookie should be accepted from server. + + Currently, pre-expired cookies never get this far -- the CookieJar + class deletes such cookies itself. + + """ + raise NotImplementedError() + + def return_ok(self, cookie, request): + """Return true if (and only if) cookie should be returned to server.""" + raise NotImplementedError() + + def domain_return_ok(self, domain, request): + """Return false if cookies should not be returned, given cookie domain. + """ + return True + + def path_return_ok(self, path, request): + """Return false if cookies should not be returned, given cookie path. + """ + return True + + +class DefaultCookiePolicy(CookiePolicy): + """Implements the standard rules for accepting and returning cookies.""" + + DomainStrictNoDots = 1 + DomainStrictNonDomain = 2 + DomainRFC2965Match = 4 + + DomainLiberal = 0 + DomainStrict = DomainStrictNoDots|DomainStrictNonDomain + + def __init__(self, + blocked_domains=None, allowed_domains=None, + netscape=True, rfc2965=False, + rfc2109_as_netscape=None, + hide_cookie2=False, + strict_domain=False, + strict_rfc2965_unverifiable=True, + strict_ns_unverifiable=False, + strict_ns_domain=DomainLiberal, + strict_ns_set_initial_dollar=False, + strict_ns_set_path=False, + ): + """Constructor arguments should be passed as keyword arguments only.""" + self.netscape = netscape + self.rfc2965 = rfc2965 + self.rfc2109_as_netscape = rfc2109_as_netscape + self.hide_cookie2 = hide_cookie2 + self.strict_domain = strict_domain + self.strict_rfc2965_unverifiable = strict_rfc2965_unverifiable + self.strict_ns_unverifiable = strict_ns_unverifiable + self.strict_ns_domain = strict_ns_domain + self.strict_ns_set_initial_dollar = strict_ns_set_initial_dollar + self.strict_ns_set_path = strict_ns_set_path + + if blocked_domains is not None: + self._blocked_domains = tuple(blocked_domains) + else: + self._blocked_domains = () + + if allowed_domains is not None: + allowed_domains = tuple(allowed_domains) + self._allowed_domains = allowed_domains + + def blocked_domains(self): + """Return the sequence of blocked domains (as a tuple).""" + return self._blocked_domains + def set_blocked_domains(self, blocked_domains): + """Set the sequence of blocked domains.""" + self._blocked_domains = tuple(blocked_domains) + + def is_blocked(self, domain): + for blocked_domain in self._blocked_domains: + if user_domain_match(domain, blocked_domain): + return True + return False + + def allowed_domains(self): + """Return None, or the sequence of allowed domains (as a tuple).""" + return self._allowed_domains + def set_allowed_domains(self, allowed_domains): + """Set the sequence of allowed domains, or None.""" + if allowed_domains is not None: + allowed_domains = tuple(allowed_domains) + self._allowed_domains = allowed_domains + + def is_not_allowed(self, domain): + if self._allowed_domains is None: + return False + for allowed_domain in self._allowed_domains: + if user_domain_match(domain, allowed_domain): + return False + return True + + def set_ok(self, cookie, request): + """ + If you override .set_ok(), be sure to call this method. If it returns + false, so should your subclass (assuming your subclass wants to be more + strict about which cookies to accept). + + """ + _debug(" - checking cookie %s=%s", cookie.name, cookie.value) + + assert cookie.name is not None + + for n in "version", "verifiability", "name", "path", "domain", "port": + fn_name = "set_ok_"+n + fn = getattr(self, fn_name) + if not fn(cookie, request): + return False + + return True + + def set_ok_version(self, cookie, request): + if cookie.version is None: + # Version is always set to 0 by parse_ns_headers if it's a Netscape + # cookie, so this must be an invalid RFC 2965 cookie. + _debug(" Set-Cookie2 without version attribute (%s=%s)", + cookie.name, cookie.value) + return False + if cookie.version > 0 and not self.rfc2965: + _debug(" RFC 2965 cookies are switched off") + return False + elif cookie.version == 0 and not self.netscape: + _debug(" Netscape cookies are switched off") + return False + return True + + def set_ok_verifiability(self, cookie, request): + if request.unverifiable and is_third_party(request): + if cookie.version > 0 and self.strict_rfc2965_unverifiable: + _debug(" third-party RFC 2965 cookie during " + "unverifiable transaction") + return False + elif cookie.version == 0 and self.strict_ns_unverifiable: + _debug(" third-party Netscape cookie during " + "unverifiable transaction") + return False + return True + + def set_ok_name(self, cookie, request): + # Try and stop servers setting V0 cookies designed to hack other + # servers that know both V0 and V1 protocols. + if (cookie.version == 0 and self.strict_ns_set_initial_dollar and + cookie.name.startswith("$")): + _debug(" illegal name (starts with '$'): '%s'", cookie.name) + return False + return True + + def set_ok_path(self, cookie, request): + if cookie.path_specified: + req_path = request_path(request) + if ((cookie.version > 0 or + (cookie.version == 0 and self.strict_ns_set_path)) and + not req_path.startswith(cookie.path)): + _debug(" path attribute %s is not a prefix of request " + "path %s", cookie.path, req_path) + return False + return True + + def set_ok_domain(self, cookie, request): + if self.is_blocked(cookie.domain): + _debug(" domain %s is in user block-list", cookie.domain) + return False + if self.is_not_allowed(cookie.domain): + _debug(" domain %s is not in user allow-list", cookie.domain) + return False + if cookie.domain_specified: + req_host, erhn = eff_request_host(request) + domain = cookie.domain + if self.strict_domain and (domain.count(".") >= 2): + # XXX This should probably be compared with the Konqueror + # (kcookiejar.cpp) and Mozilla implementations, but it's a + # losing battle. + i = domain.rfind(".") + j = domain.rfind(".", 0, i) + if j == 0: # domain like .foo.bar + tld = domain[i+1:] + sld = domain[j+1:i] + if sld.lower() in ("co", "ac", "com", "edu", "org", "net", + "gov", "mil", "int", "aero", "biz", "cat", "coop", + "info", "jobs", "mobi", "museum", "name", "pro", + "travel", "eu") and len(tld) == 2: + # domain like .co.uk + _debug(" country-code second level domain %s", domain) + return False + if domain.startswith("."): + undotted_domain = domain[1:] + else: + undotted_domain = domain + embedded_dots = (undotted_domain.find(".") >= 0) + if not embedded_dots and domain != ".local": + _debug(" non-local domain %s contains no embedded dot", + domain) + return False + if cookie.version == 0: + if (not erhn.endswith(domain) and + (not erhn.startswith(".") and + not ("."+erhn).endswith(domain))): + _debug(" effective request-host %s (even with added " + "initial dot) does not end with %s", + erhn, domain) + return False + if (cookie.version > 0 or + (self.strict_ns_domain & self.DomainRFC2965Match)): + if not domain_match(erhn, domain): + _debug(" effective request-host %s does not domain-match " + "%s", erhn, domain) + return False + if (cookie.version > 0 or + (self.strict_ns_domain & self.DomainStrictNoDots)): + host_prefix = req_host[:-len(domain)] + if (host_prefix.find(".") >= 0 and + not IPV4_RE.search(req_host)): + _debug(" host prefix %s for domain %s contains a dot", + host_prefix, domain) + return False + return True + + def set_ok_port(self, cookie, request): + if cookie.port_specified: + req_port = request_port(request) + if req_port is None: + req_port = "80" + else: + req_port = str(req_port) + for p in cookie.port.split(","): + try: + int(p) + except ValueError: + _debug(" bad port %s (not numeric)", p) + return False + if p == req_port: + break + else: + _debug(" request port (%s) not found in %s", + req_port, cookie.port) + return False + return True + + def return_ok(self, cookie, request): + """ + If you override .return_ok(), be sure to call this method. If it + returns false, so should your subclass (assuming your subclass wants to + be more strict about which cookies to return). + + """ + # Path has already been checked by .path_return_ok(), and domain + # blocking done by .domain_return_ok(). + _debug(" - checking cookie %s=%s", cookie.name, cookie.value) + + for n in "version", "verifiability", "secure", "expires", "port", "domain": + fn_name = "return_ok_"+n + fn = getattr(self, fn_name) + if not fn(cookie, request): + return False + return True + + def return_ok_version(self, cookie, request): + if cookie.version > 0 and not self.rfc2965: + _debug(" RFC 2965 cookies are switched off") + return False + elif cookie.version == 0 and not self.netscape: + _debug(" Netscape cookies are switched off") + return False + return True + + def return_ok_verifiability(self, cookie, request): + if request.unverifiable and is_third_party(request): + if cookie.version > 0 and self.strict_rfc2965_unverifiable: + _debug(" third-party RFC 2965 cookie during unverifiable " + "transaction") + return False + elif cookie.version == 0 and self.strict_ns_unverifiable: + _debug(" third-party Netscape cookie during unverifiable " + "transaction") + return False + return True + + def return_ok_secure(self, cookie, request): + if cookie.secure and request.type != "https": + _debug(" secure cookie with non-secure request") + return False + return True + + def return_ok_expires(self, cookie, request): + if cookie.is_expired(self._now): + _debug(" cookie expired") + return False + return True + + def return_ok_port(self, cookie, request): + if cookie.port: + req_port = request_port(request) + if req_port is None: + req_port = "80" + for p in cookie.port.split(","): + if p == req_port: + break + else: + _debug(" request port %s does not match cookie port %s", + req_port, cookie.port) + return False + return True + + def return_ok_domain(self, cookie, request): + req_host, erhn = eff_request_host(request) + domain = cookie.domain + + # strict check of non-domain cookies: Mozilla does this, MSIE5 doesn't + if (cookie.version == 0 and + (self.strict_ns_domain & self.DomainStrictNonDomain) and + not cookie.domain_specified and domain != erhn): + _debug(" cookie with unspecified domain does not string-compare " + "equal to request domain") + return False + + if cookie.version > 0 and not domain_match(erhn, domain): + _debug(" effective request-host name %s does not domain-match " + "RFC 2965 cookie domain %s", erhn, domain) + return False + if cookie.version == 0 and not ("."+erhn).endswith(domain): + _debug(" request-host %s does not match Netscape cookie domain " + "%s", req_host, domain) + return False + return True + + def domain_return_ok(self, domain, request): + # Liberal check of. This is here as an optimization to avoid + # having to load lots of MSIE cookie files unless necessary. + req_host, erhn = eff_request_host(request) + if not req_host.startswith("."): + req_host = "."+req_host + if not erhn.startswith("."): + erhn = "."+erhn + if not (req_host.endswith(domain) or erhn.endswith(domain)): + #_debug(" request domain %s does not match cookie domain %s", + # req_host, domain) + return False + + if self.is_blocked(domain): + _debug(" domain %s is in user block-list", domain) + return False + if self.is_not_allowed(domain): + _debug(" domain %s is not in user allow-list", domain) + return False + + return True + + def path_return_ok(self, path, request): + _debug("- checking cookie path=%s", path) + req_path = request_path(request) + if not req_path.startswith(path): + _debug(" %s does not path-match %s", req_path, path) + return False + return True + + +def vals_sorted_by_key(adict): + keys = sorted(adict.keys()) + return map(adict.get, keys) + +def deepvalues(mapping): + """Iterates over nested mapping, depth-first, in sorted order by key.""" + values = vals_sorted_by_key(mapping) + for obj in values: + mapping = False + try: + obj.items + except AttributeError: + pass + else: + mapping = True + yield from deepvalues(obj) + if not mapping: + yield obj + + +# Used as second parameter to dict.get() method, to distinguish absent +# dict key from one with a None value. +class Absent: pass + +class CookieJar: + """Collection of HTTP cookies. + + You may not need to know about this class: try + urllib.request.build_opener(HTTPCookieProcessor).open(url). + """ + + non_word_re = re.compile(r"\W") + quote_re = re.compile(r"([\"\\])") + strict_domain_re = re.compile(r"\.?[^.]*") + domain_re = re.compile(r"[^.]*") + dots_re = re.compile(r"^\.+") + + magic_re = re.compile(r"^\#LWP-Cookies-(\d+\.\d+)", re.ASCII) + + def __init__(self, policy=None): + if policy is None: + policy = DefaultCookiePolicy() + self._policy = policy + + self._cookies_lock = _threading.RLock() + self._cookies = {} + + def set_policy(self, policy): + self._policy = policy + + def _cookies_for_domain(self, domain, request): + cookies = [] + if not self._policy.domain_return_ok(domain, request): + return [] + _debug("Checking %s for cookies to return", domain) + cookies_by_path = self._cookies[domain] + for path in cookies_by_path.keys(): + if not self._policy.path_return_ok(path, request): + continue + cookies_by_name = cookies_by_path[path] + for cookie in cookies_by_name.values(): + if not self._policy.return_ok(cookie, request): + _debug(" not returning cookie") + continue + _debug(" it's a match") + cookies.append(cookie) + return cookies + + def _cookies_for_request(self, request): + """Return a list of cookies to be returned to server.""" + cookies = [] + for domain in self._cookies.keys(): + cookies.extend(self._cookies_for_domain(domain, request)) + return cookies + + def _cookie_attrs(self, cookies): + """Return a list of cookie-attributes to be returned to server. + + like ['foo="bar"; $Path="/"', ...] + + The $Version attribute is also added when appropriate (currently only + once per request). + + """ + # add cookies in order of most specific (ie. longest) path first + cookies.sort(key=lambda a: len(a.path), reverse=True) + + version_set = False + + attrs = [] + for cookie in cookies: + # set version of Cookie header + # XXX + # What should it be if multiple matching Set-Cookie headers have + # different versions themselves? + # Answer: there is no answer; was supposed to be settled by + # RFC 2965 errata, but that may never appear... + version = cookie.version + if not version_set: + version_set = True + if version > 0: + attrs.append("$Version=%s" % version) + + # quote cookie value if necessary + # (not for Netscape protocol, which already has any quotes + # intact, due to the poorly-specified Netscape Cookie: syntax) + if ((cookie.value is not None) and + self.non_word_re.search(cookie.value) and version > 0): + value = self.quote_re.sub(r"\\\1", cookie.value) + else: + value = cookie.value + + # add cookie-attributes to be returned in Cookie header + if cookie.value is None: + attrs.append(cookie.name) + else: + attrs.append("%s=%s" % (cookie.name, value)) + if version > 0: + if cookie.path_specified: + attrs.append('$Path="%s"' % cookie.path) + if cookie.domain.startswith("."): + domain = cookie.domain + if (not cookie.domain_initial_dot and + domain.startswith(".")): + domain = domain[1:] + attrs.append('$Domain="%s"' % domain) + if cookie.port is not None: + p = "$Port" + if cookie.port_specified: + p = p + ('="%s"' % cookie.port) + attrs.append(p) + + return attrs + + def add_cookie_header(self, request): + """Add correct Cookie: header to request (urllib.request.Request object). + + The Cookie2 header is also added unless policy.hide_cookie2 is true. + + """ + _debug("add_cookie_header") + self._cookies_lock.acquire() + try: + + self._policy._now = self._now = int(time.time()) + + cookies = self._cookies_for_request(request) + + attrs = self._cookie_attrs(cookies) + if attrs: + if not request.has_header("Cookie"): + request.add_unredirected_header( + "Cookie", "; ".join(attrs)) + + # if necessary, advertise that we know RFC 2965 + if (self._policy.rfc2965 and not self._policy.hide_cookie2 and + not request.has_header("Cookie2")): + for cookie in cookies: + if cookie.version != 1: + request.add_unredirected_header("Cookie2", '$Version="1"') + break + + finally: + self._cookies_lock.release() + + self.clear_expired_cookies() + + def _normalized_cookie_tuples(self, attrs_set): + """Return list of tuples containing normalised cookie information. + + attrs_set is the list of lists of key,value pairs extracted from + the Set-Cookie or Set-Cookie2 headers. + + Tuples are name, value, standard, rest, where name and value are the + cookie name and value, standard is a dictionary containing the standard + cookie-attributes (discard, secure, version, expires or max-age, + domain, path and port) and rest is a dictionary containing the rest of + the cookie-attributes. + + """ + cookie_tuples = [] + + boolean_attrs = "discard", "secure" + value_attrs = ("version", + "expires", "max-age", + "domain", "path", "port", + "comment", "commenturl") + + for cookie_attrs in attrs_set: + name, value = cookie_attrs[0] + + # Build dictionary of standard cookie-attributes (standard) and + # dictionary of other cookie-attributes (rest). + + # Note: expiry time is normalised to seconds since epoch. V0 + # cookies should have the Expires cookie-attribute, and V1 cookies + # should have Max-Age, but since V1 includes RFC 2109 cookies (and + # since V0 cookies may be a mish-mash of Netscape and RFC 2109), we + # accept either (but prefer Max-Age). + max_age_set = False + + bad_cookie = False + + standard = {} + rest = {} + for k, v in cookie_attrs[1:]: + lc = k.lower() + # don't lose case distinction for unknown fields + if lc in value_attrs or lc in boolean_attrs: + k = lc + if k in boolean_attrs and v is None: + # boolean cookie-attribute is present, but has no value + # (like "discard", rather than "port=80") + v = True + if k in standard: + # only first value is significant + continue + if k == "domain": + if v is None: + _debug(" missing value for domain attribute") + bad_cookie = True + break + # RFC 2965 section 3.3.3 + v = v.lower() + if k == "expires": + if max_age_set: + # Prefer max-age to expires (like Mozilla) + continue + if v is None: + _debug(" missing or invalid value for expires " + "attribute: treating as session cookie") + continue + if k == "max-age": + max_age_set = True + try: + v = int(v) + except ValueError: + _debug(" missing or invalid (non-numeric) value for " + "max-age attribute") + bad_cookie = True + break + # convert RFC 2965 Max-Age to seconds since epoch + # XXX Strictly you're supposed to follow RFC 2616 + # age-calculation rules. Remember that zero Max-Age + # is a request to discard (old and new) cookie, though. + k = "expires" + v = self._now + v + if (k in value_attrs) or (k in boolean_attrs): + if (v is None and + k not in ("port", "comment", "commenturl")): + _debug(" missing value for %s attribute" % k) + bad_cookie = True + break + standard[k] = v + else: + rest[k] = v + + if bad_cookie: + continue + + cookie_tuples.append((name, value, standard, rest)) + + return cookie_tuples + + def _cookie_from_cookie_tuple(self, tup, request): + # standard is dict of standard cookie-attributes, rest is dict of the + # rest of them + name, value, standard, rest = tup + + domain = standard.get("domain", Absent) + path = standard.get("path", Absent) + port = standard.get("port", Absent) + expires = standard.get("expires", Absent) + + # set the easy defaults + version = standard.get("version", None) + if version is not None: + try: + version = int(version) + except ValueError: + return None # invalid version, ignore cookie + secure = standard.get("secure", False) + # (discard is also set if expires is Absent) + discard = standard.get("discard", False) + comment = standard.get("comment", None) + comment_url = standard.get("commenturl", None) + + # set default path + if path is not Absent and path != "": + path_specified = True + path = escape_path(path) + else: + path_specified = False + path = request_path(request) + i = path.rfind("/") + if i != -1: + if version == 0: + # Netscape spec parts company from reality here + path = path[:i] + else: + path = path[:i+1] + if len(path) == 0: path = "/" + + # set default domain + domain_specified = domain is not Absent + # but first we have to remember whether it starts with a dot + domain_initial_dot = False + if domain_specified: + domain_initial_dot = bool(domain.startswith(".")) + if domain is Absent: + req_host, erhn = eff_request_host(request) + domain = erhn + elif not domain.startswith("."): + domain = "."+domain + + # set default port + port_specified = False + if port is not Absent: + if port is None: + # Port attr present, but has no value: default to request port. + # Cookie should then only be sent back on that port. + port = request_port(request) + else: + port_specified = True + port = re.sub(r"\s+", "", port) + else: + # No port attr present. Cookie can be sent back on any port. + port = None + + # set default expires and discard + if expires is Absent: + expires = None + discard = True + elif expires <= self._now: + # Expiry date in past is request to delete cookie. This can't be + # in DefaultCookiePolicy, because can't delete cookies there. + try: + self.clear(domain, path, name) + except KeyError: + pass + _debug("Expiring cookie, domain='%s', path='%s', name='%s'", + domain, path, name) + return None + + return Cookie(version, + name, value, + port, port_specified, + domain, domain_specified, domain_initial_dot, + path, path_specified, + secure, + expires, + discard, + comment, + comment_url, + rest) + + def _cookies_from_attrs_set(self, attrs_set, request): + cookie_tuples = self._normalized_cookie_tuples(attrs_set) + + cookies = [] + for tup in cookie_tuples: + cookie = self._cookie_from_cookie_tuple(tup, request) + if cookie: cookies.append(cookie) + return cookies + + def _process_rfc2109_cookies(self, cookies): + rfc2109_as_ns = getattr(self._policy, 'rfc2109_as_netscape', None) + if rfc2109_as_ns is None: + rfc2109_as_ns = not self._policy.rfc2965 + for cookie in cookies: + if cookie.version == 1: + cookie.rfc2109 = True + if rfc2109_as_ns: + # treat 2109 cookies as Netscape cookies rather than + # as RFC2965 cookies + cookie.version = 0 + + def make_cookies(self, response, request): + """Return sequence of Cookie objects extracted from response object.""" + # get cookie-attributes for RFC 2965 and Netscape protocols + headers = response.info() + rfc2965_hdrs = headers.get_all("Set-Cookie2", []) + ns_hdrs = headers.get_all("Set-Cookie", []) + + rfc2965 = self._policy.rfc2965 + netscape = self._policy.netscape + + if ((not rfc2965_hdrs and not ns_hdrs) or + (not ns_hdrs and not rfc2965) or + (not rfc2965_hdrs and not netscape) or + (not netscape and not rfc2965)): + return [] # no relevant cookie headers: quick exit + + try: + cookies = self._cookies_from_attrs_set( + split_header_words(rfc2965_hdrs), request) + except Exception: + _warn_unhandled_exception() + cookies = [] + + if ns_hdrs and netscape: + try: + # RFC 2109 and Netscape cookies + ns_cookies = self._cookies_from_attrs_set( + parse_ns_headers(ns_hdrs), request) + except Exception: + _warn_unhandled_exception() + ns_cookies = [] + self._process_rfc2109_cookies(ns_cookies) + + # Look for Netscape cookies (from Set-Cookie headers) that match + # corresponding RFC 2965 cookies (from Set-Cookie2 headers). + # For each match, keep the RFC 2965 cookie and ignore the Netscape + # cookie (RFC 2965 section 9.1). Actually, RFC 2109 cookies are + # bundled in with the Netscape cookies for this purpose, which is + # reasonable behaviour. + if rfc2965: + lookup = {} + for cookie in cookies: + lookup[(cookie.domain, cookie.path, cookie.name)] = None + + def no_matching_rfc2965(ns_cookie, lookup=lookup): + key = ns_cookie.domain, ns_cookie.path, ns_cookie.name + return key not in lookup + ns_cookies = filter(no_matching_rfc2965, ns_cookies) + + if ns_cookies: + cookies.extend(ns_cookies) + + return cookies + + def set_cookie_if_ok(self, cookie, request): + """Set a cookie if policy says it's OK to do so.""" + self._cookies_lock.acquire() + try: + self._policy._now = self._now = int(time.time()) + + if self._policy.set_ok(cookie, request): + self.set_cookie(cookie) + + + finally: + self._cookies_lock.release() + + def set_cookie(self, cookie): + """Set a cookie, without checking whether or not it should be set.""" + c = self._cookies + self._cookies_lock.acquire() + try: + if cookie.domain not in c: c[cookie.domain] = {} + c2 = c[cookie.domain] + if cookie.path not in c2: c2[cookie.path] = {} + c3 = c2[cookie.path] + c3[cookie.name] = cookie + finally: + self._cookies_lock.release() + + def extract_cookies(self, response, request): + """Extract cookies from response, where allowable given the request.""" + _debug("extract_cookies: %s", response.info()) + self._cookies_lock.acquire() + try: + self._policy._now = self._now = int(time.time()) + + for cookie in self.make_cookies(response, request): + if self._policy.set_ok(cookie, request): + _debug(" setting cookie: %s", cookie) + self.set_cookie(cookie) + finally: + self._cookies_lock.release() + + def clear(self, domain=None, path=None, name=None): + """Clear some cookies. + + Invoking this method without arguments will clear all cookies. If + given a single argument, only cookies belonging to that domain will be + removed. If given two arguments, cookies belonging to the specified + path within that domain are removed. If given three arguments, then + the cookie with the specified name, path and domain is removed. + + Raises KeyError if no matching cookie exists. + + """ + if name is not None: + if (domain is None) or (path is None): + raise ValueError( + "domain and path must be given to remove a cookie by name") + del self._cookies[domain][path][name] + elif path is not None: + if domain is None: + raise ValueError( + "domain must be given to remove cookies by path") + del self._cookies[domain][path] + elif domain is not None: + del self._cookies[domain] + else: + self._cookies = {} + + def clear_session_cookies(self): + """Discard all session cookies. + + Note that the .save() method won't save session cookies anyway, unless + you ask otherwise by passing a true ignore_discard argument. + + """ + self._cookies_lock.acquire() + try: + for cookie in self: + if cookie.discard: + self.clear(cookie.domain, cookie.path, cookie.name) + finally: + self._cookies_lock.release() + + def clear_expired_cookies(self): + """Discard all expired cookies. + + You probably don't need to call this method: expired cookies are never + sent back to the server (provided you're using DefaultCookiePolicy), + this method is called by CookieJar itself every so often, and the + .save() method won't save expired cookies anyway (unless you ask + otherwise by passing a true ignore_expires argument). + + """ + self._cookies_lock.acquire() + try: + now = time.time() + for cookie in self: + if cookie.is_expired(now): + self.clear(cookie.domain, cookie.path, cookie.name) + finally: + self._cookies_lock.release() + + def __iter__(self): + return deepvalues(self._cookies) + + def __len__(self): + """Return number of contained cookies.""" + i = 0 + for cookie in self: i = i + 1 + return i + + def __repr__(self): + r = [] + for cookie in self: r.append(repr(cookie)) + return "<%s[%s]>" % (self.__class__.__name__, ", ".join(r)) + + def __str__(self): + r = [] + for cookie in self: r.append(str(cookie)) + return "<%s[%s]>" % (self.__class__.__name__, ", ".join(r)) + + +# derives from OSError for backwards-compatibility with Python 2.4.0 +class LoadError(OSError): pass + +class FileCookieJar(CookieJar): + """CookieJar that can be loaded from and saved to a file.""" + + def __init__(self, filename=None, delayload=False, policy=None): + """ + Cookies are NOT loaded from the named file until either the .load() or + .revert() method is called. + + """ + CookieJar.__init__(self, policy) + if filename is not None: + try: + filename+"" + except: + raise ValueError("filename must be string-like") + self.filename = filename + self.delayload = bool(delayload) + + def save(self, filename=None, ignore_discard=False, ignore_expires=False): + """Save cookies to a file.""" + raise NotImplementedError() + + def load(self, filename=None, ignore_discard=False, ignore_expires=False): + """Load cookies from a file.""" + if filename is None: + if self.filename is not None: filename = self.filename + else: raise ValueError(MISSING_FILENAME_TEXT) + + with open(filename) as f: + self._really_load(f, filename, ignore_discard, ignore_expires) + + def revert(self, filename=None, + ignore_discard=False, ignore_expires=False): + """Clear all cookies and reload cookies from a saved file. + + Raises LoadError (or OSError) if reversion is not successful; the + object's state will not be altered if this happens. + + """ + if filename is None: + if self.filename is not None: filename = self.filename + else: raise ValueError(MISSING_FILENAME_TEXT) + + self._cookies_lock.acquire() + try: + + old_state = copy.deepcopy(self._cookies) + self._cookies = {} + try: + self.load(filename, ignore_discard, ignore_expires) + except OSError: + self._cookies = old_state + raise + + finally: + self._cookies_lock.release() + + +def lwp_cookie_str(cookie): + """Return string representation of Cookie in the LWP cookie file format. + + Actually, the format is extended a bit -- see module docstring. + + """ + h = [(cookie.name, cookie.value), + ("path", cookie.path), + ("domain", cookie.domain)] + if cookie.port is not None: h.append(("port", cookie.port)) + if cookie.path_specified: h.append(("path_spec", None)) + if cookie.port_specified: h.append(("port_spec", None)) + if cookie.domain_initial_dot: h.append(("domain_dot", None)) + if cookie.secure: h.append(("secure", None)) + if cookie.expires: h.append(("expires", + time2isoz(float(cookie.expires)))) + if cookie.discard: h.append(("discard", None)) + if cookie.comment: h.append(("comment", cookie.comment)) + if cookie.comment_url: h.append(("commenturl", cookie.comment_url)) + + keys = sorted(cookie._rest.keys()) + for k in keys: + h.append((k, str(cookie._rest[k]))) + + h.append(("version", str(cookie.version))) + + return join_header_words([h]) + +class LWPCookieJar(FileCookieJar): + """ + The LWPCookieJar saves a sequence of "Set-Cookie3" lines. + "Set-Cookie3" is the format used by the libwww-perl library, not known + to be compatible with any browser, but which is easy to read and + doesn't lose information about RFC 2965 cookies. + + Additional methods + + as_lwp_str(ignore_discard=True, ignore_expired=True) + + """ + + def as_lwp_str(self, ignore_discard=True, ignore_expires=True): + """Return cookies as a string of "\\n"-separated "Set-Cookie3" headers. + + ignore_discard and ignore_expires: see docstring for FileCookieJar.save + + """ + now = time.time() + r = [] + for cookie in self: + if not ignore_discard and cookie.discard: + continue + if not ignore_expires and cookie.is_expired(now): + continue + r.append("Set-Cookie3: %s" % lwp_cookie_str(cookie)) + return "\n".join(r+[""]) + + def save(self, filename=None, ignore_discard=False, ignore_expires=False): + if filename is None: + if self.filename is not None: filename = self.filename + else: raise ValueError(MISSING_FILENAME_TEXT) + + with open(filename, "w") as f: + # There really isn't an LWP Cookies 2.0 format, but this indicates + # that there is extra information in here (domain_dot and + # port_spec) while still being compatible with libwww-perl, I hope. + f.write("#LWP-Cookies-2.0\n") + f.write(self.as_lwp_str(ignore_discard, ignore_expires)) + + def _really_load(self, f, filename, ignore_discard, ignore_expires): + magic = f.readline() + if not self.magic_re.search(magic): + msg = ("%r does not look like a Set-Cookie3 (LWP) format " + "file" % filename) + raise LoadError(msg) + + now = time.time() + + header = "Set-Cookie3:" + boolean_attrs = ("port_spec", "path_spec", "domain_dot", + "secure", "discard") + value_attrs = ("version", + "port", "path", "domain", + "expires", + "comment", "commenturl") + + try: + while 1: + line = f.readline() + if line == "": break + if not line.startswith(header): + continue + line = line[len(header):].strip() + + for data in split_header_words([line]): + name, value = data[0] + standard = {} + rest = {} + for k in boolean_attrs: + standard[k] = False + for k, v in data[1:]: + if k is not None: + lc = k.lower() + else: + lc = None + # don't lose case distinction for unknown fields + if (lc in value_attrs) or (lc in boolean_attrs): + k = lc + if k in boolean_attrs: + if v is None: v = True + standard[k] = v + elif k in value_attrs: + standard[k] = v + else: + rest[k] = v + + h = standard.get + expires = h("expires") + discard = h("discard") + if expires is not None: + expires = iso2time(expires) + if expires is None: + discard = True + domain = h("domain") + domain_specified = domain.startswith(".") + c = Cookie(h("version"), name, value, + h("port"), h("port_spec"), + domain, domain_specified, h("domain_dot"), + h("path"), h("path_spec"), + h("secure"), + expires, + discard, + h("comment"), + h("commenturl"), + rest) + if not ignore_discard and c.discard: + continue + if not ignore_expires and c.is_expired(now): + continue + self.set_cookie(c) + except OSError: + raise + except Exception: + _warn_unhandled_exception() + raise LoadError("invalid Set-Cookie3 format file %r: %r" % + (filename, line)) + + +class MozillaCookieJar(FileCookieJar): + """ + + WARNING: you may want to backup your browser's cookies file if you use + this class to save cookies. I *think* it works, but there have been + bugs in the past! + + This class differs from CookieJar only in the format it uses to save and + load cookies to and from a file. This class uses the Mozilla/Netscape + `cookies.txt' format. lynx uses this file format, too. + + Don't expect cookies saved while the browser is running to be noticed by + the browser (in fact, Mozilla on unix will overwrite your saved cookies if + you change them on disk while it's running; on Windows, you probably can't + save at all while the browser is running). + + Note that the Mozilla/Netscape format will downgrade RFC2965 cookies to + Netscape cookies on saving. + + In particular, the cookie version and port number information is lost, + together with information about whether or not Path, Port and Discard were + specified by the Set-Cookie2 (or Set-Cookie) header, and whether or not the + domain as set in the HTTP header started with a dot (yes, I'm aware some + domains in Netscape files start with a dot and some don't -- trust me, you + really don't want to know any more about this). + + Note that though Mozilla and Netscape use the same format, they use + slightly different headers. The class saves cookies using the Netscape + header by default (Mozilla can cope with that). + + """ + magic_re = re.compile("#( Netscape)? HTTP Cookie File") + header = """\ +# Netscape HTTP Cookie File +# http://curl.haxx.se/rfc/cookie_spec.html +# This is a generated file! Do not edit. + +""" + + def _really_load(self, f, filename, ignore_discard, ignore_expires): + now = time.time() + + magic = f.readline() + if not self.magic_re.search(magic): + raise LoadError( + "%r does not look like a Netscape format cookies file" % + filename) + + try: + while 1: + line = f.readline() + if line == "": break + + # last field may be absent, so keep any trailing tab + if line.endswith("\n"): line = line[:-1] + + # skip comments and blank lines XXX what is $ for? + if (line.strip().startswith(("#", "$")) or + line.strip() == ""): + continue + + domain, domain_specified, path, secure, expires, name, value = \ + line.split("\t") + secure = (secure == "TRUE") + domain_specified = (domain_specified == "TRUE") + if name == "": + # cookies.txt regards 'Set-Cookie: foo' as a cookie + # with no name, whereas http.cookiejar regards it as a + # cookie with no value. + name = value + value = None + + initial_dot = domain.startswith(".") + assert domain_specified == initial_dot + + discard = False + if expires == "": + expires = None + discard = True + + # assume path_specified is false + c = Cookie(0, name, value, + None, False, + domain, domain_specified, initial_dot, + path, False, + secure, + expires, + discard, + None, + None, + {}) + if not ignore_discard and c.discard: + continue + if not ignore_expires and c.is_expired(now): + continue + self.set_cookie(c) + + except OSError: + raise + except Exception: + _warn_unhandled_exception() + raise LoadError("invalid Netscape format cookies file %r: %r" % + (filename, line)) + + def save(self, filename=None, ignore_discard=False, ignore_expires=False): + if filename is None: + if self.filename is not None: filename = self.filename + else: raise ValueError(MISSING_FILENAME_TEXT) + + with open(filename, "w") as f: + f.write(self.header) + now = time.time() + for cookie in self: + if not ignore_discard and cookie.discard: + continue + if not ignore_expires and cookie.is_expired(now): + continue + if cookie.secure: secure = "TRUE" + else: secure = "FALSE" + if cookie.domain.startswith("."): initial_dot = "TRUE" + else: initial_dot = "FALSE" + if cookie.expires is not None: + expires = str(cookie.expires) + else: + expires = "" + if cookie.value is None: + # cookies.txt regards 'Set-Cookie: foo' as a cookie + # with no name, whereas http.cookiejar regards it as a + # cookie with no value. + name = "" + value = cookie.name + else: + name = cookie.name + value = cookie.value + f.write( + "\t".join([cookie.domain, initial_dot, cookie.path, + secure, expires, name, value])+ + "\n") diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/http/cookies.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/http/cookies.py new file mode 100644 index 0000000..d93cd71 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/http/cookies.py @@ -0,0 +1,691 @@ +# This is part of Python source code with Eventlet-specific modifications. +# +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights +# Reserved +# +# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +# -------------------------------------------- +# +# 1. This LICENSE AGREEMENT is between the Python Software Foundation +# ("PSF"), and the Individual or Organization ("Licensee") accessing and +# otherwise using this software ("Python") in source or binary form and +# its associated documentation. +# +# 2. Subject to the terms and conditions of this License Agreement, PSF hereby +# grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +# analyze, test, perform and/or display publicly, prepare derivative works, +# distribute, and otherwise use Python alone or in any derivative version, +# provided, however, that PSF's License Agreement and PSF's notice of copyright, +# i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights +# Reserved" are retained in Python alone or in any derivative version prepared by +# Licensee. +# +# 3. In the event Licensee prepares a derivative work that is based on +# or incorporates Python or any part thereof, and wants to make +# the derivative work available to others as provided herein, then +# Licensee hereby agrees to include in any such work a brief summary of +# the changes made to Python. +# +# 4. PSF is making Python available to Licensee on an "AS IS" +# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +# INFRINGE ANY THIRD PARTY RIGHTS. +# +# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. +# +# 6. This License Agreement will automatically terminate upon a material +# breach of its terms and conditions. +# +# 7. Nothing in this License Agreement shall be deemed to create any +# relationship of agency, partnership, or joint venture between PSF and +# Licensee. This License Agreement does not grant permission to use PSF +# trademarks or trade name in a trademark sense to endorse or promote +# products or services of Licensee, or any third party. +# +# 8. By copying, installing or otherwise using Python, Licensee +# agrees to be bound by the terms and conditions of this License +# Agreement. +#### +# Copyright 2000 by Timothy O'Malley +# +# All Rights Reserved +# +# Permission to use, copy, modify, and distribute this software +# and its documentation for any purpose and without fee is hereby +# granted, provided that the above copyright notice appear in all +# copies and that both that copyright notice and this permission +# notice appear in supporting documentation, and that the name of +# Timothy O'Malley not be used in advertising or publicity +# pertaining to distribution of the software without specific, written +# prior permission. +# +# Timothy O'Malley DISCLAIMS ALL WARRANTIES WITH REGARD TO THIS +# SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +# AND FITNESS, IN NO EVENT SHALL Timothy O'Malley BE LIABLE FOR +# ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, +# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS +# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +# PERFORMANCE OF THIS SOFTWARE. +# +#### +# +# Id: Cookie.py,v 2.29 2000/08/23 05:28:49 timo Exp +# by Timothy O'Malley +# +# Cookie.py is a Python module for the handling of HTTP +# cookies as a Python dictionary. See RFC 2109 for more +# information on cookies. +# +# The original idea to treat Cookies as a dictionary came from +# Dave Mitchell (davem@magnet.com) in 1995, when he released the +# first version of nscookie.py. +# +#### + +r""" +Here's a sample session to show how to use this module. +At the moment, this is the only documentation. + +The Basics +---------- + +Importing is easy... + + >>> from http import cookies + +Most of the time you start by creating a cookie. + + >>> C = cookies.SimpleCookie() + +Once you've created your Cookie, you can add values just as if it were +a dictionary. + + >>> C = cookies.SimpleCookie() + >>> C["fig"] = "newton" + >>> C["sugar"] = "wafer" + >>> C.output() + 'Set-Cookie: fig=newton\r\nSet-Cookie: sugar=wafer' + +Notice that the printable representation of a Cookie is the +appropriate format for a Set-Cookie: header. This is the +default behavior. You can change the header and printed +attributes by using the .output() function + + >>> C = cookies.SimpleCookie() + >>> C["rocky"] = "road" + >>> C["rocky"]["path"] = "/cookie" + >>> print(C.output(header="Cookie:")) + Cookie: rocky=road; Path=/cookie + >>> print(C.output(attrs=[], header="Cookie:")) + Cookie: rocky=road + +The load() method of a Cookie extracts cookies from a string. In a +CGI script, you would use this method to extract the cookies from the +HTTP_COOKIE environment variable. + + >>> C = cookies.SimpleCookie() + >>> C.load("chips=ahoy; vienna=finger") + >>> C.output() + 'Set-Cookie: chips=ahoy\r\nSet-Cookie: vienna=finger' + +The load() method is darn-tootin smart about identifying cookies +within a string. Escaped quotation marks, nested semicolons, and other +such trickeries do not confuse it. + + >>> C = cookies.SimpleCookie() + >>> C.load('keebler="E=everybody; L=\\"Loves\\"; fudge=\\012;";') + >>> print(C) + Set-Cookie: keebler="E=everybody; L=\"Loves\"; fudge=\012;" + +Each element of the Cookie also supports all of the RFC 2109 +Cookie attributes. Here's an example which sets the Path +attribute. + + >>> C = cookies.SimpleCookie() + >>> C["oreo"] = "doublestuff" + >>> C["oreo"]["path"] = "/" + >>> print(C) + Set-Cookie: oreo=doublestuff; Path=/ + +Each dictionary element has a 'value' attribute, which gives you +back the value associated with the key. + + >>> C = cookies.SimpleCookie() + >>> C["twix"] = "none for you" + >>> C["twix"].value + 'none for you' + +The SimpleCookie expects that all values should be standard strings. +Just to be sure, SimpleCookie invokes the str() builtin to convert +the value to a string, when the values are set dictionary-style. + + >>> C = cookies.SimpleCookie() + >>> C["number"] = 7 + >>> C["string"] = "seven" + >>> C["number"].value + '7' + >>> C["string"].value + 'seven' + >>> C.output() + 'Set-Cookie: number=7\r\nSet-Cookie: string=seven' + +Finis. +""" + +# +# Import our required modules +# +import re +import string + +__all__ = ["CookieError", "BaseCookie", "SimpleCookie"] + +_nulljoin = ''.join +_semispacejoin = '; '.join +_spacejoin = ' '.join + +def _warn_deprecated_setter(setter): + import warnings + msg = ('The .%s setter is deprecated. The attribute will be read-only in ' + 'future releases. Please use the set() method instead.' % setter) + warnings.warn(msg, DeprecationWarning, stacklevel=3) + +# +# Define an exception visible to External modules +# +class CookieError(Exception): + pass + + +# These quoting routines conform to the RFC2109 specification, which in +# turn references the character definitions from RFC2068. They provide +# a two-way quoting algorithm. Any non-text character is translated +# into a 4 character sequence: a forward-slash followed by the +# three-digit octal equivalent of the character. Any '\' or '"' is +# quoted with a preceding '\' slash. +# Because of the way browsers really handle cookies (as opposed to what +# the RFC says) we also encode "," and ";". +# +# These are taken from RFC2068 and RFC2109. +# _LegalChars is the list of chars which don't require "'s +# _Translator hash-table for fast quoting +# +_LegalChars = string.ascii_letters + string.digits + "!#$%&'*+-.^_`|~:" +_UnescapedChars = _LegalChars + ' ()/<=>?@[]{}' + +_Translator = {n: '\\%03o' % n + for n in set(range(256)) - set(map(ord, _UnescapedChars))} +_Translator.update({ + ord('"'): '\\"', + ord('\\'): '\\\\', +}) + +# Eventlet change: match used instead of fullmatch for Python 3.3 compatibility +_is_legal_key = re.compile(r'[%s]+\Z' % re.escape(_LegalChars)).match + +def _quote(str): + r"""Quote a string for use in a cookie header. + + If the string does not need to be double-quoted, then just return the + string. Otherwise, surround the string in doublequotes and quote + (with a \) special characters. + """ + if str is None or _is_legal_key(str): + return str + else: + return '"' + str.translate(_Translator) + '"' + + +_OctalPatt = re.compile(r"\\[0-3][0-7][0-7]") +_QuotePatt = re.compile(r"[\\].") + +def _unquote(str): + # If there aren't any doublequotes, + # then there can't be any special characters. See RFC 2109. + if str is None or len(str) < 2: + return str + if str[0] != '"' or str[-1] != '"': + return str + + # We have to assume that we must decode this string. + # Down to work. + + # Remove the "s + str = str[1:-1] + + # Check for special sequences. Examples: + # \012 --> \n + # \" --> " + # + i = 0 + n = len(str) + res = [] + while 0 <= i < n: + o_match = _OctalPatt.search(str, i) + q_match = _QuotePatt.search(str, i) + if not o_match and not q_match: # Neither matched + res.append(str[i:]) + break + # else: + j = k = -1 + if o_match: + j = o_match.start(0) + if q_match: + k = q_match.start(0) + if q_match and (not o_match or k < j): # QuotePatt matched + res.append(str[i:k]) + res.append(str[k+1]) + i = k + 2 + else: # OctalPatt matched + res.append(str[i:j]) + res.append(chr(int(str[j+1:j+4], 8))) + i = j + 4 + return _nulljoin(res) + +# The _getdate() routine is used to set the expiration time in the cookie's HTTP +# header. By default, _getdate() returns the current time in the appropriate +# "expires" format for a Set-Cookie header. The one optional argument is an +# offset from now, in seconds. For example, an offset of -3600 means "one hour +# ago". The offset may be a floating point number. +# + +_weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + +_monthname = [None, + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + +def _getdate(future=0, weekdayname=_weekdayname, monthname=_monthname): + from eventlet.green.time import gmtime, time + now = time() + year, month, day, hh, mm, ss, wd, y, z = gmtime(now + future) + return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % \ + (weekdayname[wd], day, monthname[month], year, hh, mm, ss) + + +class Morsel(dict): + """A class to hold ONE (key, value) pair. + + In a cookie, each such pair may have several attributes, so this class is + used to keep the attributes associated with the appropriate key,value pair. + This class also includes a coded_value attribute, which is used to hold + the network representation of the value. This is most useful when Python + objects are pickled for network transit. + """ + # RFC 2109 lists these attributes as reserved: + # path comment domain + # max-age secure version + # + # For historical reasons, these attributes are also reserved: + # expires + # + # This is an extension from Microsoft: + # httponly + # + # This dictionary provides a mapping from the lowercase + # variant on the left to the appropriate traditional + # formatting on the right. + _reserved = { + "expires" : "expires", + "path" : "Path", + "comment" : "Comment", + "domain" : "Domain", + "max-age" : "Max-Age", + "secure" : "Secure", + "httponly" : "HttpOnly", + "version" : "Version", + } + + _flags = {'secure', 'httponly'} + + def __init__(self): + # Set defaults + self._key = self._value = self._coded_value = None + + # Set default attributes + for key in self._reserved: + dict.__setitem__(self, key, "") + + @property + def key(self): + return self._key + + @key.setter + def key(self, key): + _warn_deprecated_setter('key') + self._key = key + + @property + def value(self): + return self._value + + @value.setter + def value(self, value): + _warn_deprecated_setter('value') + self._value = value + + @property + def coded_value(self): + return self._coded_value + + @coded_value.setter + def coded_value(self, coded_value): + _warn_deprecated_setter('coded_value') + self._coded_value = coded_value + + def __setitem__(self, K, V): + K = K.lower() + if not K in self._reserved: + raise CookieError("Invalid attribute %r" % (K,)) + dict.__setitem__(self, K, V) + + def setdefault(self, key, val=None): + key = key.lower() + if key not in self._reserved: + raise CookieError("Invalid attribute %r" % (key,)) + return dict.setdefault(self, key, val) + + def __eq__(self, morsel): + if not isinstance(morsel, Morsel): + return NotImplemented + return (dict.__eq__(self, morsel) and + self._value == morsel._value and + self._key == morsel._key and + self._coded_value == morsel._coded_value) + + __ne__ = object.__ne__ + + def copy(self): + morsel = Morsel() + dict.update(morsel, self) + morsel.__dict__.update(self.__dict__) + return morsel + + def update(self, values): + data = {} + for key, val in dict(values).items(): + key = key.lower() + if key not in self._reserved: + raise CookieError("Invalid attribute %r" % (key,)) + data[key] = val + dict.update(self, data) + + def isReservedKey(self, K): + return K.lower() in self._reserved + + def set(self, key, val, coded_val, LegalChars=_LegalChars): + if LegalChars != _LegalChars: + import warnings + warnings.warn( + 'LegalChars parameter is deprecated, ignored and will ' + 'be removed in future versions.', DeprecationWarning, + stacklevel=2) + + if key.lower() in self._reserved: + raise CookieError('Attempt to set a reserved key %r' % (key,)) + if not _is_legal_key(key): + raise CookieError('Illegal key %r' % (key,)) + + # It's a good key, so save it. + self._key = key + self._value = val + self._coded_value = coded_val + + def __getstate__(self): + return { + 'key': self._key, + 'value': self._value, + 'coded_value': self._coded_value, + } + + def __setstate__(self, state): + self._key = state['key'] + self._value = state['value'] + self._coded_value = state['coded_value'] + + def output(self, attrs=None, header="Set-Cookie:"): + return "%s %s" % (header, self.OutputString(attrs)) + + __str__ = output + + def __repr__(self): + return '<%s: %s>' % (self.__class__.__name__, self.OutputString()) + + def js_output(self, attrs=None): + # Print javascript + return """ + + """ % (self.OutputString(attrs).replace('"', r'\"')) + + def OutputString(self, attrs=None): + # Build up our result + # + result = [] + append = result.append + + # First, the key=value pair + append("%s=%s" % (self.key, self.coded_value)) + + # Now add any defined attributes + if attrs is None: + attrs = self._reserved + items = sorted(self.items()) + for key, value in items: + if value == "": + continue + if key not in attrs: + continue + if key == "expires" and isinstance(value, int): + append("%s=%s" % (self._reserved[key], _getdate(value))) + elif key == "max-age" and isinstance(value, int): + append("%s=%d" % (self._reserved[key], value)) + elif key in self._flags: + if value: + append(str(self._reserved[key])) + else: + append("%s=%s" % (self._reserved[key], value)) + + # Return the result + return _semispacejoin(result) + + +# +# Pattern for finding cookie +# +# This used to be strict parsing based on the RFC2109 and RFC2068 +# specifications. I have since discovered that MSIE 3.0x doesn't +# follow the character rules outlined in those specs. As a +# result, the parsing rules here are less strict. +# + +_LegalKeyChars = r"\w\d!#%&'~_`><@,:/\$\*\+\-\.\^\|\)\(\?\}\{\=" +_LegalValueChars = _LegalKeyChars + r'\[\]' +_CookiePattern = re.compile(r""" + (?x) # This is a verbose pattern + \s* # Optional whitespace at start of cookie + (?P # Start of group 'key' + [""" + _LegalKeyChars + r"""]+? # Any word of at least one letter + ) # End of group 'key' + ( # Optional group: there may not be a value. + \s*=\s* # Equal Sign + (?P # Start of group 'val' + "(?:[^\\"]|\\.)*" # Any doublequoted string + | # or + \w{3},\s[\w\d\s-]{9,11}\s[\d:]{8}\sGMT # Special case for "expires" attr + | # or + [""" + _LegalValueChars + r"""]* # Any word or empty string + ) # End of group 'val' + )? # End of optional value group + \s* # Any number of spaces. + (\s+|;|$) # Ending either at space, semicolon, or EOS. + """, re.ASCII) # May be removed if safe. + + +# At long last, here is the cookie class. Using this class is almost just like +# using a dictionary. See this module's docstring for example usage. +# +class BaseCookie(dict): + """A container class for a set of Morsels.""" + + def value_decode(self, val): + """real_value, coded_value = value_decode(STRING) + Called prior to setting a cookie's value from the network + representation. The VALUE is the value read from HTTP + header. + Override this function to modify the behavior of cookies. + """ + return val, val + + def value_encode(self, val): + """real_value, coded_value = value_encode(VALUE) + Called prior to setting a cookie's value from the dictionary + representation. The VALUE is the value being assigned. + Override this function to modify the behavior of cookies. + """ + strval = str(val) + return strval, strval + + def __init__(self, input=None): + if input: + self.load(input) + + def __set(self, key, real_value, coded_value): + """Private method for setting a cookie's value""" + M = self.get(key, Morsel()) + M.set(key, real_value, coded_value) + dict.__setitem__(self, key, M) + + def __setitem__(self, key, value): + """Dictionary style assignment.""" + if isinstance(value, Morsel): + # allow assignment of constructed Morsels (e.g. for pickling) + dict.__setitem__(self, key, value) + else: + rval, cval = self.value_encode(value) + self.__set(key, rval, cval) + + def output(self, attrs=None, header="Set-Cookie:", sep="\015\012"): + """Return a string suitable for HTTP.""" + result = [] + items = sorted(self.items()) + for key, value in items: + result.append(value.output(attrs, header)) + return sep.join(result) + + __str__ = output + + def __repr__(self): + l = [] + items = sorted(self.items()) + for key, value in items: + l.append('%s=%s' % (key, repr(value.value))) + return '<%s: %s>' % (self.__class__.__name__, _spacejoin(l)) + + def js_output(self, attrs=None): + """Return a string suitable for JavaScript.""" + result = [] + items = sorted(self.items()) + for key, value in items: + result.append(value.js_output(attrs)) + return _nulljoin(result) + + def load(self, rawdata): + """Load cookies from a string (presumably HTTP_COOKIE) or + from a dictionary. Loading cookies from a dictionary 'd' + is equivalent to calling: + map(Cookie.__setitem__, d.keys(), d.values()) + """ + if isinstance(rawdata, str): + self.__parse_string(rawdata) + else: + # self.update() wouldn't call our custom __setitem__ + for key, value in rawdata.items(): + self[key] = value + return + + def __parse_string(self, str, patt=_CookiePattern): + i = 0 # Our starting point + n = len(str) # Length of string + parsed_items = [] # Parsed (type, key, value) triples + morsel_seen = False # A key=value pair was previously encountered + + TYPE_ATTRIBUTE = 1 + TYPE_KEYVALUE = 2 + + # We first parse the whole cookie string and reject it if it's + # syntactically invalid (this helps avoid some classes of injection + # attacks). + while 0 <= i < n: + # Start looking for a cookie + match = patt.match(str, i) + if not match: + # No more cookies + break + + key, value = match.group("key"), match.group("val") + i = match.end(0) + + if key[0] == "$": + if not morsel_seen: + # We ignore attributes which pertain to the cookie + # mechanism as a whole, such as "$Version". + # See RFC 2965. (Does anyone care?) + continue + parsed_items.append((TYPE_ATTRIBUTE, key[1:], value)) + elif key.lower() in Morsel._reserved: + if not morsel_seen: + # Invalid cookie string + return + if value is None: + if key.lower() in Morsel._flags: + parsed_items.append((TYPE_ATTRIBUTE, key, True)) + else: + # Invalid cookie string + return + else: + parsed_items.append((TYPE_ATTRIBUTE, key, _unquote(value))) + elif value is not None: + parsed_items.append((TYPE_KEYVALUE, key, self.value_decode(value))) + morsel_seen = True + else: + # Invalid cookie string + return + + # The cookie string is valid, apply it. + M = None # current morsel + for tp, key, value in parsed_items: + if tp == TYPE_ATTRIBUTE: + assert M is not None + M[key] = value + else: + assert tp == TYPE_KEYVALUE + rval, cval = value + self.__set(key, rval, cval) + M = self[key] + + +class SimpleCookie(BaseCookie): + """ + SimpleCookie supports strings as cookie values. When setting + the value using the dictionary assignment notation, SimpleCookie + calls the builtin str() to convert the value to a string. Values + received from HTTP are kept as strings. + """ + def value_decode(self, val): + return _unquote(val), val + + def value_encode(self, val): + strval = str(val) + return strval, _quote(strval) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/http/server.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/http/server.py new file mode 100644 index 0000000..190bdb9 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/http/server.py @@ -0,0 +1,1266 @@ +# This is part of Python source code with Eventlet-specific modifications. +# +# Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights +# Reserved +# +# PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +# -------------------------------------------- +# +# 1. This LICENSE AGREEMENT is between the Python Software Foundation +# ("PSF"), and the Individual or Organization ("Licensee") accessing and +# otherwise using this software ("Python") in source or binary form and +# its associated documentation. +# +# 2. Subject to the terms and conditions of this License Agreement, PSF hereby +# grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +# analyze, test, perform and/or display publicly, prepare derivative works, +# distribute, and otherwise use Python alone or in any derivative version, +# provided, however, that PSF's License Agreement and PSF's notice of copyright, +# i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +# 2011, 2012, 2013, 2014, 2015, 2016 Python Software Foundation; All Rights +# Reserved" are retained in Python alone or in any derivative version prepared by +# Licensee. +# +# 3. In the event Licensee prepares a derivative work that is based on +# or incorporates Python or any part thereof, and wants to make +# the derivative work available to others as provided herein, then +# Licensee hereby agrees to include in any such work a brief summary of +# the changes made to Python. +# +# 4. PSF is making Python available to Licensee on an "AS IS" +# basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +# IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +# DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +# FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +# INFRINGE ANY THIRD PARTY RIGHTS. +# +# 5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +# FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +# A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +# OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. +# +# 6. This License Agreement will automatically terminate upon a material +# breach of its terms and conditions. +# +# 7. Nothing in this License Agreement shall be deemed to create any +# relationship of agency, partnership, or joint venture between PSF and +# Licensee. This License Agreement does not grant permission to use PSF +# trademarks or trade name in a trademark sense to endorse or promote +# products or services of Licensee, or any third party. +# +# 8. By copying, installing or otherwise using Python, Licensee +# agrees to be bound by the terms and conditions of this License +# Agreement. +"""HTTP server classes. + +Note: BaseHTTPRequestHandler doesn't implement any HTTP request; see +SimpleHTTPRequestHandler for simple implementations of GET, HEAD and POST, +and CGIHTTPRequestHandler for CGI scripts. + +It does, however, optionally implement HTTP/1.1 persistent connections, +as of version 0.3. + +Notes on CGIHTTPRequestHandler +------------------------------ + +This class implements GET and POST requests to cgi-bin scripts. + +If the os.fork() function is not present (e.g. on Windows), +subprocess.Popen() is used as a fallback, with slightly altered semantics. + +In all cases, the implementation is intentionally naive -- all +requests are executed synchronously. + +SECURITY WARNING: DON'T USE THIS CODE UNLESS YOU ARE INSIDE A FIREWALL +-- it may execute arbitrary Python code or external programs. + +Note that status code 200 is sent prior to execution of a CGI script, so +scripts cannot send other status codes such as 302 (redirect). + +XXX To do: + +- log requests even later (to capture byte count) +- log user-agent header and other interesting goodies +- send error log to separate file +""" + + +# See also: +# +# HTTP Working Group T. Berners-Lee +# INTERNET-DRAFT R. T. Fielding +# H. Frystyk Nielsen +# Expires September 8, 1995 March 8, 1995 +# +# URL: http://www.ics.uci.edu/pub/ietf/http/draft-ietf-http-v10-spec-00.txt +# +# and +# +# Network Working Group R. Fielding +# Request for Comments: 2616 et al +# Obsoletes: 2068 June 1999 +# Category: Standards Track +# +# URL: http://www.faqs.org/rfcs/rfc2616.html + +# Log files +# --------- +# +# Here's a quote from the NCSA httpd docs about log file format. +# +# | The logfile format is as follows. Each line consists of: +# | +# | host rfc931 authuser [DD/Mon/YYYY:hh:mm:ss] "request" ddd bbbb +# | +# | host: Either the DNS name or the IP number of the remote client +# | rfc931: Any information returned by identd for this person, +# | - otherwise. +# | authuser: If user sent a userid for authentication, the user name, +# | - otherwise. +# | DD: Day +# | Mon: Month (calendar name) +# | YYYY: Year +# | hh: hour (24-hour format, the machine's timezone) +# | mm: minutes +# | ss: seconds +# | request: The first line of the HTTP request as sent by the client. +# | ddd: the status code returned by the server, - if not available. +# | bbbb: the total number of bytes sent, +# | *not including the HTTP/1.0 header*, - if not available +# | +# | You can determine the name of the file accessed through request. +# +# (Actually, the latter is only true if you know the server configuration +# at the time the request was made!) + +__version__ = "0.6" + +__all__ = [ + "HTTPServer", "BaseHTTPRequestHandler", + "SimpleHTTPRequestHandler", "CGIHTTPRequestHandler", +] + +import email.utils +import html +import io +import mimetypes +import posixpath +import shutil +import sys +import urllib.parse +import copy +import argparse + +from eventlet.green import ( + os, + time, + select, + socket, + SocketServer as socketserver, + subprocess, +) +from eventlet.green.http import client as http_client, HTTPStatus + + +# Default error message template +DEFAULT_ERROR_MESSAGE = """\ + + + + + Error response + + +

Error response

+

Error code: %(code)d

+

Message: %(message)s.

+

Error code explanation: %(code)s - %(explain)s.

+ + +""" + +DEFAULT_ERROR_CONTENT_TYPE = "text/html;charset=utf-8" + +class HTTPServer(socketserver.TCPServer): + + allow_reuse_address = 1 # Seems to make sense in testing environment + + def server_bind(self): + """Override server_bind to store the server name.""" + socketserver.TCPServer.server_bind(self) + host, port = self.server_address[:2] + self.server_name = socket.getfqdn(host) + self.server_port = port + + +class BaseHTTPRequestHandler(socketserver.StreamRequestHandler): + + """HTTP request handler base class. + + The following explanation of HTTP serves to guide you through the + code as well as to expose any misunderstandings I may have about + HTTP (so you don't need to read the code to figure out I'm wrong + :-). + + HTTP (HyperText Transfer Protocol) is an extensible protocol on + top of a reliable stream transport (e.g. TCP/IP). The protocol + recognizes three parts to a request: + + 1. One line identifying the request type and path + 2. An optional set of RFC-822-style headers + 3. An optional data part + + The headers and data are separated by a blank line. + + The first line of the request has the form + + + + where is a (case-sensitive) keyword such as GET or POST, + is a string containing path information for the request, + and should be the string "HTTP/1.0" or "HTTP/1.1". + is encoded using the URL encoding scheme (using %xx to signify + the ASCII character with hex code xx). + + The specification specifies that lines are separated by CRLF but + for compatibility with the widest range of clients recommends + servers also handle LF. Similarly, whitespace in the request line + is treated sensibly (allowing multiple spaces between components + and allowing trailing whitespace). + + Similarly, for output, lines ought to be separated by CRLF pairs + but most clients grok LF characters just fine. + + If the first line of the request has the form + + + + (i.e. is left out) then this is assumed to be an HTTP + 0.9 request; this form has no optional headers and data part and + the reply consists of just the data. + + The reply form of the HTTP 1.x protocol again has three parts: + + 1. One line giving the response code + 2. An optional set of RFC-822-style headers + 3. The data + + Again, the headers and data are separated by a blank line. + + The response code line has the form + + + + where is the protocol version ("HTTP/1.0" or "HTTP/1.1"), + is a 3-digit response code indicating success or + failure of the request, and is an optional + human-readable string explaining what the response code means. + + This server parses the request and the headers, and then calls a + function specific to the request type (). Specifically, + a request SPAM will be handled by a method do_SPAM(). If no + such method exists the server sends an error response to the + client. If it exists, it is called with no arguments: + + do_SPAM() + + Note that the request name is case sensitive (i.e. SPAM and spam + are different requests). + + The various request details are stored in instance variables: + + - client_address is the client IP address in the form (host, + port); + + - command, path and version are the broken-down request line; + + - headers is an instance of email.message.Message (or a derived + class) containing the header information; + + - rfile is a file object open for reading positioned at the + start of the optional input data part; + + - wfile is a file object open for writing. + + IT IS IMPORTANT TO ADHERE TO THE PROTOCOL FOR WRITING! + + The first thing to be written must be the response line. Then + follow 0 or more header lines, then a blank line, and then the + actual data (if any). The meaning of the header lines depends on + the command executed by the server; in most cases, when data is + returned, there should be at least one header line of the form + + Content-type: / + + where and should be registered MIME types, + e.g. "text/html" or "text/plain". + + """ + + # The Python system version, truncated to its first component. + sys_version = "Python/" + sys.version.split()[0] + + # The server software version. You may want to override this. + # The format is multiple whitespace-separated strings, + # where each string is of the form name[/version]. + server_version = "BaseHTTP/" + __version__ + + error_message_format = DEFAULT_ERROR_MESSAGE + error_content_type = DEFAULT_ERROR_CONTENT_TYPE + + # The default request version. This only affects responses up until + # the point where the request line is parsed, so it mainly decides what + # the client gets back when sending a malformed request line. + # Most web servers default to HTTP 0.9, i.e. don't send a status line. + default_request_version = "HTTP/0.9" + + def parse_request(self): + """Parse a request (internal). + + The request should be stored in self.raw_requestline; the results + are in self.command, self.path, self.request_version and + self.headers. + + Return True for success, False for failure; on failure, an + error is sent back. + + """ + self.command = None # set in case of error on the first line + self.request_version = version = self.default_request_version + self.close_connection = True + requestline = str(self.raw_requestline, 'iso-8859-1') + requestline = requestline.rstrip('\r\n') + self.requestline = requestline + words = requestline.split() + if len(words) == 3: + command, path, version = words + try: + if version[:5] != 'HTTP/': + raise ValueError + base_version_number = version.split('/', 1)[1] + version_number = base_version_number.split(".") + # RFC 2145 section 3.1 says there can be only one "." and + # - major and minor numbers MUST be treated as + # separate integers; + # - HTTP/2.4 is a lower version than HTTP/2.13, which in + # turn is lower than HTTP/12.3; + # - Leading zeros MUST be ignored by recipients. + if len(version_number) != 2: + raise ValueError + version_number = int(version_number[0]), int(version_number[1]) + except (ValueError, IndexError): + self.send_error( + HTTPStatus.BAD_REQUEST, + "Bad request version (%r)" % version) + return False + if version_number >= (1, 1) and self.protocol_version >= "HTTP/1.1": + self.close_connection = False + if version_number >= (2, 0): + self.send_error( + HTTPStatus.HTTP_VERSION_NOT_SUPPORTED, + "Invalid HTTP version (%s)" % base_version_number) + return False + elif len(words) == 2: + command, path = words + self.close_connection = True + if command != 'GET': + self.send_error( + HTTPStatus.BAD_REQUEST, + "Bad HTTP/0.9 request type (%r)" % command) + return False + elif not words: + return False + else: + self.send_error( + HTTPStatus.BAD_REQUEST, + "Bad request syntax (%r)" % requestline) + return False + self.command, self.path, self.request_version = command, path, version + + # Examine the headers and look for a Connection directive. + try: + self.headers = http_client.parse_headers(self.rfile, + _class=self.MessageClass) + except http_client.LineTooLong as err: + self.send_error( + HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, + "Line too long", + str(err)) + return False + except http_client.HTTPException as err: + self.send_error( + HTTPStatus.REQUEST_HEADER_FIELDS_TOO_LARGE, + "Too many headers", + str(err) + ) + return False + + conntype = self.headers.get('Connection', "") + if conntype.lower() == 'close': + self.close_connection = True + elif (conntype.lower() == 'keep-alive' and + self.protocol_version >= "HTTP/1.1"): + self.close_connection = False + # Examine the headers and look for an Expect directive + expect = self.headers.get('Expect', "") + if (expect.lower() == "100-continue" and + self.protocol_version >= "HTTP/1.1" and + self.request_version >= "HTTP/1.1"): + if not self.handle_expect_100(): + return False + return True + + def handle_expect_100(self): + """Decide what to do with an "Expect: 100-continue" header. + + If the client is expecting a 100 Continue response, we must + respond with either a 100 Continue or a final response before + waiting for the request body. The default is to always respond + with a 100 Continue. You can behave differently (for example, + reject unauthorized requests) by overriding this method. + + This method should either return True (possibly after sending + a 100 Continue response) or send an error response and return + False. + + """ + self.send_response_only(HTTPStatus.CONTINUE) + self.end_headers() + return True + + def handle_one_request(self): + """Handle a single HTTP request. + + You normally don't need to override this method; see the class + __doc__ string for information on how to handle specific HTTP + commands such as GET and POST. + + """ + try: + self.raw_requestline = self.rfile.readline(65537) + if len(self.raw_requestline) > 65536: + self.requestline = '' + self.request_version = '' + self.command = '' + self.send_error(HTTPStatus.REQUEST_URI_TOO_LONG) + return + if not self.raw_requestline: + self.close_connection = True + return + if not self.parse_request(): + # An error code has been sent, just exit + return + mname = 'do_' + self.command + if not hasattr(self, mname): + self.send_error( + HTTPStatus.NOT_IMPLEMENTED, + "Unsupported method (%r)" % self.command) + return + method = getattr(self, mname) + method() + self.wfile.flush() #actually send the response if not already done. + except socket.timeout as e: + #a read or a write timed out. Discard this connection + self.log_error("Request timed out: %r", e) + self.close_connection = True + return + + def handle(self): + """Handle multiple requests if necessary.""" + self.close_connection = True + + self.handle_one_request() + while not self.close_connection: + self.handle_one_request() + + def send_error(self, code, message=None, explain=None): + """Send and log an error reply. + + Arguments are + * code: an HTTP error code + 3 digits + * message: a simple optional 1 line reason phrase. + *( HTAB / SP / VCHAR / %x80-FF ) + defaults to short entry matching the response code + * explain: a detailed message defaults to the long entry + matching the response code. + + This sends an error response (so it must be called before any + output has been generated), logs the error, and finally sends + a piece of HTML explaining the error to the user. + + """ + + try: + shortmsg, longmsg = self.responses[code] + except KeyError: + shortmsg, longmsg = '???', '???' + if message is None: + message = shortmsg + if explain is None: + explain = longmsg + self.log_error("code %d, message %s", code, message) + self.send_response(code, message) + self.send_header('Connection', 'close') + + # Message body is omitted for cases described in: + # - RFC7230: 3.3. 1xx, 204(No Content), 304(Not Modified) + # - RFC7231: 6.3.6. 205(Reset Content) + body = None + if (code >= 200 and + code not in (HTTPStatus.NO_CONTENT, + HTTPStatus.RESET_CONTENT, + HTTPStatus.NOT_MODIFIED)): + # HTML encode to prevent Cross Site Scripting attacks + # (see bug #1100201) + content = (self.error_message_format % { + 'code': code, + 'message': html.escape(message, quote=False), + 'explain': html.escape(explain, quote=False) + }) + body = content.encode('UTF-8', 'replace') + self.send_header("Content-Type", self.error_content_type) + self.send_header('Content-Length', int(len(body))) + self.end_headers() + + if self.command != 'HEAD' and body: + self.wfile.write(body) + + def send_response(self, code, message=None): + """Add the response header to the headers buffer and log the + response code. + + Also send two standard headers with the server software + version and the current date. + + """ + self.log_request(code) + self.send_response_only(code, message) + self.send_header('Server', self.version_string()) + self.send_header('Date', self.date_time_string()) + + def send_response_only(self, code, message=None): + """Send the response header only.""" + if self.request_version != 'HTTP/0.9': + if message is None: + if code in self.responses: + message = self.responses[code][0] + else: + message = '' + if not hasattr(self, '_headers_buffer'): + self._headers_buffer = [] + self._headers_buffer.append(("%s %d %s\r\n" % + (self.protocol_version, code, message)).encode( + 'latin-1', 'strict')) + + def send_header(self, keyword, value): + """Send a MIME header to the headers buffer.""" + if self.request_version != 'HTTP/0.9': + if not hasattr(self, '_headers_buffer'): + self._headers_buffer = [] + self._headers_buffer.append( + ("%s: %s\r\n" % (keyword, value)).encode('latin-1', 'strict')) + + if keyword.lower() == 'connection': + if value.lower() == 'close': + self.close_connection = True + elif value.lower() == 'keep-alive': + self.close_connection = False + + def end_headers(self): + """Send the blank line ending the MIME headers.""" + if self.request_version != 'HTTP/0.9': + self._headers_buffer.append(b"\r\n") + self.flush_headers() + + def flush_headers(self): + if hasattr(self, '_headers_buffer'): + self.wfile.write(b"".join(self._headers_buffer)) + self._headers_buffer = [] + + def log_request(self, code='-', size='-'): + """Log an accepted request. + + This is called by send_response(). + + """ + if isinstance(code, HTTPStatus): + code = code.value + self.log_message('"%s" %s %s', + self.requestline, str(code), str(size)) + + def log_error(self, format, *args): + """Log an error. + + This is called when a request cannot be fulfilled. By + default it passes the message on to log_message(). + + Arguments are the same as for log_message(). + + XXX This should go to the separate error log. + + """ + + self.log_message(format, *args) + + def log_message(self, format, *args): + """Log an arbitrary message. + + This is used by all other logging functions. Override + it if you have specific logging wishes. + + The first argument, FORMAT, is a format string for the + message to be logged. If the format string contains + any % escapes requiring parameters, they should be + specified as subsequent arguments (it's just like + printf!). + + The client ip and current date/time are prefixed to + every message. + + """ + + sys.stderr.write("%s - - [%s] %s\n" % + (self.address_string(), + self.log_date_time_string(), + format%args)) + + def version_string(self): + """Return the server software version string.""" + return self.server_version + ' ' + self.sys_version + + def date_time_string(self, timestamp=None): + """Return the current date and time formatted for a message header.""" + if timestamp is None: + timestamp = time.time() + return email.utils.formatdate(timestamp, usegmt=True) + + def log_date_time_string(self): + """Return the current time formatted for logging.""" + now = time.time() + year, month, day, hh, mm, ss, x, y, z = time.localtime(now) + s = "%02d/%3s/%04d %02d:%02d:%02d" % ( + day, self.monthname[month], year, hh, mm, ss) + return s + + weekdayname = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] + + monthname = [None, + 'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', + 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'] + + def address_string(self): + """Return the client address.""" + + return self.client_address[0] + + # Essentially static class variables + + # The version of the HTTP protocol we support. + # Set this to HTTP/1.1 to enable automatic keepalive + protocol_version = "HTTP/1.0" + + # MessageClass used to parse headers + MessageClass = http_client.HTTPMessage + + # hack to maintain backwards compatibility + responses = { + v: (v.phrase, v.description) + for v in HTTPStatus.__members__.values() + } + + +class SimpleHTTPRequestHandler(BaseHTTPRequestHandler): + + """Simple HTTP request handler with GET and HEAD commands. + + This serves files from the current directory and any of its + subdirectories. The MIME type for files is determined by + calling the .guess_type() method. + + The GET and HEAD requests are identical except that the HEAD + request omits the actual contents of the file. + + """ + + server_version = "SimpleHTTP/" + __version__ + + def do_GET(self): + """Serve a GET request.""" + f = self.send_head() + if f: + try: + self.copyfile(f, self.wfile) + finally: + f.close() + + def do_HEAD(self): + """Serve a HEAD request.""" + f = self.send_head() + if f: + f.close() + + def send_head(self): + """Common code for GET and HEAD commands. + + This sends the response code and MIME headers. + + Return value is either a file object (which has to be copied + to the outputfile by the caller unless the command was HEAD, + and must be closed by the caller under all circumstances), or + None, in which case the caller has nothing further to do. + + """ + path = self.translate_path(self.path) + f = None + if os.path.isdir(path): + parts = urllib.parse.urlsplit(self.path) + if not parts.path.endswith('/'): + # redirect browser - doing basically what apache does + self.send_response(HTTPStatus.MOVED_PERMANENTLY) + new_parts = (parts[0], parts[1], parts[2] + '/', + parts[3], parts[4]) + new_url = urllib.parse.urlunsplit(new_parts) + self.send_header("Location", new_url) + self.end_headers() + return None + for index in "index.html", "index.htm": + index = os.path.join(path, index) + if os.path.exists(index): + path = index + break + else: + return self.list_directory(path) + ctype = self.guess_type(path) + try: + f = open(path, 'rb') + except OSError: + self.send_error(HTTPStatus.NOT_FOUND, "File not found") + return None + try: + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", ctype) + fs = os.fstat(f.fileno()) + self.send_header("Content-Length", str(fs[6])) + self.send_header("Last-Modified", self.date_time_string(fs.st_mtime)) + self.end_headers() + return f + except: + f.close() + raise + + def list_directory(self, path): + """Helper to produce a directory listing (absent index.html). + + Return value is either a file object, or None (indicating an + error). In either case, the headers are sent, making the + interface the same as for send_head(). + + """ + try: + list = os.listdir(path) + except OSError: + self.send_error( + HTTPStatus.NOT_FOUND, + "No permission to list directory") + return None + list.sort(key=lambda a: a.lower()) + r = [] + try: + displaypath = urllib.parse.unquote(self.path, + errors='surrogatepass') + except UnicodeDecodeError: + displaypath = urllib.parse.unquote(path) + displaypath = html.escape(displaypath, quote=False) + enc = sys.getfilesystemencoding() + title = 'Directory listing for %s' % displaypath + r.append('') + r.append('\n') + r.append('' % enc) + r.append('%s\n' % title) + r.append('\n

%s

' % title) + r.append('
\n
    ') + for name in list: + fullname = os.path.join(path, name) + displayname = linkname = name + # Append / for directories or @ for symbolic links + if os.path.isdir(fullname): + displayname = name + "/" + linkname = name + "/" + if os.path.islink(fullname): + displayname = name + "@" + # Note: a link to a directory displays with @ and links with / + r.append('
  • %s
  • ' + % (urllib.parse.quote(linkname, + errors='surrogatepass'), + html.escape(displayname, quote=False))) + r.append('
\n
\n\n\n') + encoded = '\n'.join(r).encode(enc, 'surrogateescape') + f = io.BytesIO() + f.write(encoded) + f.seek(0) + self.send_response(HTTPStatus.OK) + self.send_header("Content-type", "text/html; charset=%s" % enc) + self.send_header("Content-Length", str(len(encoded))) + self.end_headers() + return f + + def translate_path(self, path): + """Translate a /-separated PATH to the local filename syntax. + + Components that mean special things to the local file system + (e.g. drive or directory names) are ignored. (XXX They should + probably be diagnosed.) + + """ + # abandon query parameters + path = path.split('?',1)[0] + path = path.split('#',1)[0] + # Don't forget explicit trailing slash when normalizing. Issue17324 + trailing_slash = path.rstrip().endswith('/') + try: + path = urllib.parse.unquote(path, errors='surrogatepass') + except UnicodeDecodeError: + path = urllib.parse.unquote(path) + path = posixpath.normpath(path) + words = path.split('/') + words = filter(None, words) + path = os.getcwd() + for word in words: + if os.path.dirname(word) or word in (os.curdir, os.pardir): + # Ignore components that are not a simple file/directory name + continue + path = os.path.join(path, word) + if trailing_slash: + path += '/' + return path + + def copyfile(self, source, outputfile): + """Copy all data between two file objects. + + The SOURCE argument is a file object open for reading + (or anything with a read() method) and the DESTINATION + argument is a file object open for writing (or + anything with a write() method). + + The only reason for overriding this would be to change + the block size or perhaps to replace newlines by CRLF + -- note however that this the default server uses this + to copy binary data as well. + + """ + shutil.copyfileobj(source, outputfile) + + def guess_type(self, path): + """Guess the type of a file. + + Argument is a PATH (a filename). + + Return value is a string of the form type/subtype, + usable for a MIME Content-type header. + + The default implementation looks the file's extension + up in the table self.extensions_map, using application/octet-stream + as a default; however it would be permissible (if + slow) to look inside the data to make a better guess. + + """ + + base, ext = posixpath.splitext(path) + if ext in self.extensions_map: + return self.extensions_map[ext] + ext = ext.lower() + if ext in self.extensions_map: + return self.extensions_map[ext] + else: + return self.extensions_map[''] + + if not mimetypes.inited: + mimetypes.init() # try to read system mime.types + extensions_map = mimetypes.types_map.copy() + extensions_map.update({ + '': 'application/octet-stream', # Default + '.py': 'text/plain', + '.c': 'text/plain', + '.h': 'text/plain', + }) + + +# Utilities for CGIHTTPRequestHandler + +def _url_collapse_path(path): + """ + Given a URL path, remove extra '/'s and '.' path elements and collapse + any '..' references and returns a collapsed path. + + Implements something akin to RFC-2396 5.2 step 6 to parse relative paths. + The utility of this function is limited to is_cgi method and helps + preventing some security attacks. + + Returns: The reconstituted URL, which will always start with a '/'. + + Raises: IndexError if too many '..' occur within the path. + + """ + # Query component should not be involved. + path, _, query = path.partition('?') + path = urllib.parse.unquote(path) + + # Similar to os.path.split(os.path.normpath(path)) but specific to URL + # path semantics rather than local operating system semantics. + path_parts = path.split('/') + head_parts = [] + for part in path_parts[:-1]: + if part == '..': + head_parts.pop() # IndexError if more '..' than prior parts + elif part and part != '.': + head_parts.append( part ) + if path_parts: + tail_part = path_parts.pop() + if tail_part: + if tail_part == '..': + head_parts.pop() + tail_part = '' + elif tail_part == '.': + tail_part = '' + else: + tail_part = '' + + if query: + tail_part = '?'.join((tail_part, query)) + + splitpath = ('/' + '/'.join(head_parts), tail_part) + collapsed_path = "/".join(splitpath) + + return collapsed_path + + + +nobody = None + +def nobody_uid(): + """Internal routine to get nobody's uid""" + global nobody + if nobody: + return nobody + try: + import pwd + except ImportError: + return -1 + try: + nobody = pwd.getpwnam('nobody')[2] + except KeyError: + nobody = 1 + max(x[2] for x in pwd.getpwall()) + return nobody + + +def executable(path): + """Test for executable file.""" + return os.access(path, os.X_OK) + + +class CGIHTTPRequestHandler(SimpleHTTPRequestHandler): + + """Complete HTTP server with GET, HEAD and POST commands. + + GET and HEAD also support running CGI scripts. + + The POST command is *only* implemented for CGI scripts. + + """ + + # Determine platform specifics + have_fork = hasattr(os, 'fork') + + # Make rfile unbuffered -- we need to read one line and then pass + # the rest to a subprocess, so we can't use buffered input. + rbufsize = 0 + + def do_POST(self): + """Serve a POST request. + + This is only implemented for CGI scripts. + + """ + + if self.is_cgi(): + self.run_cgi() + else: + self.send_error( + HTTPStatus.NOT_IMPLEMENTED, + "Can only POST to CGI scripts") + + def send_head(self): + """Version of send_head that support CGI scripts""" + if self.is_cgi(): + return self.run_cgi() + else: + return SimpleHTTPRequestHandler.send_head(self) + + def is_cgi(self): + """Test whether self.path corresponds to a CGI script. + + Returns True and updates the cgi_info attribute to the tuple + (dir, rest) if self.path requires running a CGI script. + Returns False otherwise. + + If any exception is raised, the caller should assume that + self.path was rejected as invalid and act accordingly. + + The default implementation tests whether the normalized url + path begins with one of the strings in self.cgi_directories + (and the next character is a '/' or the end of the string). + + """ + collapsed_path = _url_collapse_path(self.path) + dir_sep = collapsed_path.find('/', 1) + head, tail = collapsed_path[:dir_sep], collapsed_path[dir_sep+1:] + if head in self.cgi_directories: + self.cgi_info = head, tail + return True + return False + + + cgi_directories = ['/cgi-bin', '/htbin'] + + def is_executable(self, path): + """Test whether argument path is an executable file.""" + return executable(path) + + def is_python(self, path): + """Test whether argument path is a Python script.""" + head, tail = os.path.splitext(path) + return tail.lower() in (".py", ".pyw") + + def run_cgi(self): + """Execute a CGI script.""" + dir, rest = self.cgi_info + path = dir + '/' + rest + i = path.find('/', len(dir)+1) + while i >= 0: + nextdir = path[:i] + nextrest = path[i+1:] + + scriptdir = self.translate_path(nextdir) + if os.path.isdir(scriptdir): + dir, rest = nextdir, nextrest + i = path.find('/', len(dir)+1) + else: + break + + # find an explicit query string, if present. + rest, _, query = rest.partition('?') + + # dissect the part after the directory name into a script name & + # a possible additional path, to be stored in PATH_INFO. + i = rest.find('/') + if i >= 0: + script, rest = rest[:i], rest[i:] + else: + script, rest = rest, '' + + scriptname = dir + '/' + script + scriptfile = self.translate_path(scriptname) + if not os.path.exists(scriptfile): + self.send_error( + HTTPStatus.NOT_FOUND, + "No such CGI script (%r)" % scriptname) + return + if not os.path.isfile(scriptfile): + self.send_error( + HTTPStatus.FORBIDDEN, + "CGI script is not a plain file (%r)" % scriptname) + return + ispy = self.is_python(scriptname) + if self.have_fork or not ispy: + if not self.is_executable(scriptfile): + self.send_error( + HTTPStatus.FORBIDDEN, + "CGI script is not executable (%r)" % scriptname) + return + + # Reference: http://hoohoo.ncsa.uiuc.edu/cgi/env.html + # XXX Much of the following could be prepared ahead of time! + env = copy.deepcopy(os.environ) + env['SERVER_SOFTWARE'] = self.version_string() + env['SERVER_NAME'] = self.server.server_name + env['GATEWAY_INTERFACE'] = 'CGI/1.1' + env['SERVER_PROTOCOL'] = self.protocol_version + env['SERVER_PORT'] = str(self.server.server_port) + env['REQUEST_METHOD'] = self.command + uqrest = urllib.parse.unquote(rest) + env['PATH_INFO'] = uqrest + env['PATH_TRANSLATED'] = self.translate_path(uqrest) + env['SCRIPT_NAME'] = scriptname + if query: + env['QUERY_STRING'] = query + env['REMOTE_ADDR'] = self.client_address[0] + authorization = self.headers.get("authorization") + if authorization: + authorization = authorization.split() + if len(authorization) == 2: + import base64, binascii + env['AUTH_TYPE'] = authorization[0] + if authorization[0].lower() == "basic": + try: + authorization = authorization[1].encode('ascii') + authorization = base64.decodebytes(authorization).\ + decode('ascii') + except (binascii.Error, UnicodeError): + pass + else: + authorization = authorization.split(':') + if len(authorization) == 2: + env['REMOTE_USER'] = authorization[0] + # XXX REMOTE_IDENT + if self.headers.get('content-type') is None: + env['CONTENT_TYPE'] = self.headers.get_content_type() + else: + env['CONTENT_TYPE'] = self.headers['content-type'] + length = self.headers.get('content-length') + if length: + env['CONTENT_LENGTH'] = length + referer = self.headers.get('referer') + if referer: + env['HTTP_REFERER'] = referer + accept = [] + for line in self.headers.getallmatchingheaders('accept'): + if line[:1] in "\t\n\r ": + accept.append(line.strip()) + else: + accept = accept + line[7:].split(',') + env['HTTP_ACCEPT'] = ','.join(accept) + ua = self.headers.get('user-agent') + if ua: + env['HTTP_USER_AGENT'] = ua + co = filter(None, self.headers.get_all('cookie', [])) + cookie_str = ', '.join(co) + if cookie_str: + env['HTTP_COOKIE'] = cookie_str + # XXX Other HTTP_* headers + # Since we're setting the env in the parent, provide empty + # values to override previously set values + for k in ('QUERY_STRING', 'REMOTE_HOST', 'CONTENT_LENGTH', + 'HTTP_USER_AGENT', 'HTTP_COOKIE', 'HTTP_REFERER'): + env.setdefault(k, "") + + self.send_response(HTTPStatus.OK, "Script output follows") + self.flush_headers() + + decoded_query = query.replace('+', ' ') + + if self.have_fork: + # Unix -- fork as we should + args = [script] + if '=' not in decoded_query: + args.append(decoded_query) + nobody = nobody_uid() + self.wfile.flush() # Always flush before forking + pid = os.fork() + if pid != 0: + # Parent + pid, sts = os.waitpid(pid, 0) + # throw away additional data [see bug #427345] + while select.select([self.rfile], [], [], 0)[0]: + if not self.rfile.read(1): + break + if sts: + self.log_error("CGI script exit status %#x", sts) + return + # Child + try: + try: + os.setuid(nobody) + except OSError: + pass + os.dup2(self.rfile.fileno(), 0) + os.dup2(self.wfile.fileno(), 1) + os.execve(scriptfile, args, env) + except: + self.server.handle_error(self.request, self.client_address) + os._exit(127) + + else: + # Non-Unix -- use subprocess + cmdline = [scriptfile] + if self.is_python(scriptfile): + interp = sys.executable + if interp.lower().endswith("w.exe"): + # On Windows, use python.exe, not pythonw.exe + interp = interp[:-5] + interp[-4:] + cmdline = [interp, '-u'] + cmdline + if '=' not in query: + cmdline.append(query) + self.log_message("command: %s", subprocess.list2cmdline(cmdline)) + try: + nbytes = int(length) + except (TypeError, ValueError): + nbytes = 0 + p = subprocess.Popen(cmdline, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + env = env + ) + if self.command.lower() == "post" and nbytes > 0: + data = self.rfile.read(nbytes) + else: + data = None + # throw away additional data [see bug #427345] + while select.select([self.rfile._sock], [], [], 0)[0]: + if not self.rfile._sock.recv(1): + break + stdout, stderr = p.communicate(data) + self.wfile.write(stdout) + if stderr: + self.log_error('%s', stderr) + p.stderr.close() + p.stdout.close() + status = p.returncode + if status: + self.log_error("CGI script exit status %#x", status) + else: + self.log_message("CGI script exited OK") + + +def test(HandlerClass=BaseHTTPRequestHandler, + ServerClass=HTTPServer, protocol="HTTP/1.0", port=8000, bind=""): + """Test the HTTP request handler class. + + This runs an HTTP server on port 8000 (or the port argument). + + """ + server_address = (bind, port) + + HandlerClass.protocol_version = protocol + with ServerClass(server_address, HandlerClass) as httpd: + sa = httpd.socket.getsockname() + serve_message = "Serving HTTP on {host} port {port} (http://{host}:{port}/) ..." + print(serve_message.format(host=sa[0], port=sa[1])) + try: + httpd.serve_forever() + except KeyboardInterrupt: + print("\nKeyboard interrupt received, exiting.") + sys.exit(0) + +if __name__ == '__main__': + parser = argparse.ArgumentParser() + parser.add_argument('--cgi', action='store_true', + help='Run as CGI Server') + parser.add_argument('--bind', '-b', default='', metavar='ADDRESS', + help='Specify alternate bind address ' + '[default: all interfaces]') + parser.add_argument('port', action='store', + default=8000, type=int, + nargs='?', + help='Specify alternate port [default: 8000]') + args = parser.parse_args() + if args.cgi: + handler_class = CGIHTTPRequestHandler + else: + handler_class = SimpleHTTPRequestHandler + test(HandlerClass=handler_class, port=args.port, bind=args.bind) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/httplib.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/httplib.py new file mode 100644 index 0000000..f67dbfe --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/httplib.py @@ -0,0 +1,18 @@ +from eventlet import patcher +from eventlet.green import socket + +to_patch = [('socket', socket)] + +try: + from eventlet.green import ssl + to_patch.append(('ssl', ssl)) +except ImportError: + pass + +from eventlet.green.http import client +for name in dir(client): + if name not in patcher.__exclude: + globals()[name] = getattr(client, name) + +if __name__ == '__main__': + test() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/os.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/os.py new file mode 100644 index 0000000..5942f36 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/os.py @@ -0,0 +1,133 @@ +os_orig = __import__("os") +import errno +socket = __import__("socket") +from stat import S_ISREG + +from eventlet import greenio +from eventlet.support import get_errno +from eventlet import greenthread +from eventlet import hubs +from eventlet.patcher import slurp_properties + +__all__ = os_orig.__all__ +__patched__ = ['fdopen', 'read', 'write', 'wait', 'waitpid', 'open'] + +slurp_properties( + os_orig, + globals(), + ignore=__patched__, + srckeys=dir(os_orig)) + + +def fdopen(fd, *args, **kw): + """fdopen(fd [, mode='r' [, bufsize]]) -> file_object + + Return an open file object connected to a file descriptor.""" + if not isinstance(fd, int): + raise TypeError('fd should be int, not %r' % fd) + try: + return greenio.GreenPipe(fd, *args, **kw) + except OSError as e: + raise OSError(*e.args) + + +__original_read__ = os_orig.read + + +def read(fd, n): + """read(fd, buffersize) -> string + + Read a file descriptor.""" + while True: + # don't wait to read for regular files + # select/poll will always return True while epoll will simply crash + st_mode = os_orig.stat(fd).st_mode + if not S_ISREG(st_mode): + try: + hubs.trampoline(fd, read=True) + except hubs.IOClosed: + return '' + + try: + return __original_read__(fd, n) + except OSError as e: + if get_errno(e) == errno.EPIPE: + return '' + if get_errno(e) != errno.EAGAIN: + raise + + +__original_write__ = os_orig.write + + +def write(fd, st): + """write(fd, string) -> byteswritten + + Write a string to a file descriptor. + """ + while True: + # don't wait to write for regular files + # select/poll will always return True while epoll will simply crash + st_mode = os_orig.stat(fd).st_mode + if not S_ISREG(st_mode): + try: + hubs.trampoline(fd, write=True) + except hubs.IOClosed: + return 0 + + try: + return __original_write__(fd, st) + except OSError as e: + if get_errno(e) not in [errno.EAGAIN, errno.EPIPE]: + raise + + +def wait(): + """wait() -> (pid, status) + + Wait for completion of a child process.""" + return waitpid(0, 0) + + +__original_waitpid__ = os_orig.waitpid + + +def waitpid(pid, options): + """waitpid(...) + waitpid(pid, options) -> (pid, status) + + Wait for completion of a given child process.""" + if options & os_orig.WNOHANG != 0: + return __original_waitpid__(pid, options) + else: + new_options = options | os_orig.WNOHANG + while True: + rpid, status = __original_waitpid__(pid, new_options) + if rpid and status >= 0: + return rpid, status + greenthread.sleep(0.01) + + +__original_open__ = os_orig.open + + +def open(file, flags, mode=0o777, dir_fd=None): + """ Wrap os.open + This behaves identically, but collaborates with + the hub's notify_opened protocol. + """ + # pathlib workaround #534 pathlib._NormalAccessor wraps `open` in + # `staticmethod` for py < 3.7 but not 3.7. That means we get here with + # `file` being a pathlib._NormalAccessor object, and the other arguments + # shifted. Fortunately pathlib doesn't use the `dir_fd` argument, so we + # have space in the parameter list. We use some heuristics to detect this + # and adjust the parameters (without importing pathlib) + if type(file).__name__ == '_NormalAccessor': + file, flags, mode, dir_fd = flags, mode, dir_fd, None + + if dir_fd is not None: + fd = __original_open__(file, flags, mode, dir_fd=dir_fd) + else: + fd = __original_open__(file, flags, mode) + hubs.notify_opened(fd) + return fd diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/profile.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/profile.py new file mode 100644 index 0000000..a03b507 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/profile.py @@ -0,0 +1,257 @@ +# Copyright (c) 2010, CCP Games +# 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 CCP Games 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 CCP GAMES ``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 CCP GAMES 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. + +"""This module is API-equivalent to the standard library :mod:`profile` module +lbut it is greenthread-aware as well as thread-aware. Use this module +to profile Eventlet-based applications in preference to either :mod:`profile` or :mod:`cProfile`. +FIXME: No testcases for this module. +""" + +profile_orig = __import__('profile') +__all__ = profile_orig.__all__ + +from eventlet.patcher import slurp_properties +slurp_properties(profile_orig, globals(), srckeys=dir(profile_orig)) + +import sys +import functools + +from eventlet import greenthread +from eventlet import patcher +import _thread + +thread = patcher.original(_thread.__name__) # non-monkeypatched module needed + + +# This class provides the start() and stop() functions +class Profile(profile_orig.Profile): + base = profile_orig.Profile + + def __init__(self, timer=None, bias=None): + self.current_tasklet = greenthread.getcurrent() + self.thread_id = thread.get_ident() + self.base.__init__(self, timer, bias) + self.sleeping = {} + + def __call__(self, *args): + """make callable, allowing an instance to be the profiler""" + self.dispatcher(*args) + + def _setup(self): + self._has_setup = True + self.cur = None + self.timings = {} + self.current_tasklet = greenthread.getcurrent() + self.thread_id = thread.get_ident() + self.simulate_call("profiler") + + def start(self, name="start"): + if getattr(self, "running", False): + return + self._setup() + self.simulate_call("start") + self.running = True + sys.setprofile(self.dispatcher) + + def stop(self): + sys.setprofile(None) + self.running = False + self.TallyTimings() + + # special cases for the original run commands, makin sure to + # clear the timer context. + def runctx(self, cmd, globals, locals): + if not getattr(self, "_has_setup", False): + self._setup() + try: + return profile_orig.Profile.runctx(self, cmd, globals, locals) + finally: + self.TallyTimings() + + def runcall(self, func, *args, **kw): + if not getattr(self, "_has_setup", False): + self._setup() + try: + return profile_orig.Profile.runcall(self, func, *args, **kw) + finally: + self.TallyTimings() + + def trace_dispatch_return_extend_back(self, frame, t): + """A hack function to override error checking in parent class. It + allows invalid returns (where frames weren't preveiously entered into + the profiler) which can happen for all the tasklets that suddenly start + to get monitored. This means that the time will eventually be attributed + to a call high in the chain, when there is a tasklet switch + """ + if isinstance(self.cur[-2], Profile.fake_frame): + return False + self.trace_dispatch_call(frame, 0) + return self.trace_dispatch_return(frame, t) + + def trace_dispatch_c_return_extend_back(self, frame, t): + # same for c return + if isinstance(self.cur[-2], Profile.fake_frame): + return False # ignore bogus returns + self.trace_dispatch_c_call(frame, 0) + return self.trace_dispatch_return(frame, t) + + def SwitchTasklet(self, t0, t1, t): + # tally the time spent in the old tasklet + pt, it, et, fn, frame, rcur = self.cur + cur = (pt, it + t, et, fn, frame, rcur) + + # we are switching to a new tasklet, store the old + self.sleeping[t0] = cur, self.timings + self.current_tasklet = t1 + + # find the new one + try: + self.cur, self.timings = self.sleeping.pop(t1) + except KeyError: + self.cur, self.timings = None, {} + self.simulate_call("profiler") + self.simulate_call("new_tasklet") + + def TallyTimings(self): + oldtimings = self.sleeping + self.sleeping = {} + + # first, unwind the main "cur" + self.cur = self.Unwind(self.cur, self.timings) + + # we must keep the timings dicts separate for each tasklet, since it contains + # the 'ns' item, recursion count of each function in that tasklet. This is + # used in the Unwind dude. + for tasklet, (cur, timings) in oldtimings.items(): + self.Unwind(cur, timings) + + for k, v in timings.items(): + if k not in self.timings: + self.timings[k] = v + else: + # accumulate all to the self.timings + cc, ns, tt, ct, callers = self.timings[k] + # ns should be 0 after unwinding + cc += v[0] + tt += v[2] + ct += v[3] + for k1, v1 in v[4].items(): + callers[k1] = callers.get(k1, 0) + v1 + self.timings[k] = cc, ns, tt, ct, callers + + def Unwind(self, cur, timings): + "A function to unwind a 'cur' frame and tally the results" + "see profile.trace_dispatch_return() for details" + # also see simulate_cmd_complete() + while(cur[-1]): + rpt, rit, ret, rfn, frame, rcur = cur + frame_total = rit + ret + + if rfn in timings: + cc, ns, tt, ct, callers = timings[rfn] + else: + cc, ns, tt, ct, callers = 0, 0, 0, 0, {} + + if not ns: + ct = ct + frame_total + cc = cc + 1 + + if rcur: + ppt, pit, pet, pfn, pframe, pcur = rcur + else: + pfn = None + + if pfn in callers: + callers[pfn] = callers[pfn] + 1 # hack: gather more + elif pfn: + callers[pfn] = 1 + + timings[rfn] = cc, ns - 1, tt + rit, ct, callers + + ppt, pit, pet, pfn, pframe, pcur = rcur + rcur = ppt, pit + rpt, pet + frame_total, pfn, pframe, pcur + cur = rcur + return cur + + +def ContextWrap(f): + @functools.wraps(f) + def ContextWrapper(self, arg, t): + current = greenthread.getcurrent() + if current != self.current_tasklet: + self.SwitchTasklet(self.current_tasklet, current, t) + t = 0.0 # the time was billed to the previous tasklet + return f(self, arg, t) + return ContextWrapper + + +# Add "return safety" to the dispatchers +Profile.dispatch = dict(profile_orig.Profile.dispatch, **{ + 'return': Profile.trace_dispatch_return_extend_back, + 'c_return': Profile.trace_dispatch_c_return_extend_back, +}) +# Add automatic tasklet detection to the callbacks. +Profile.dispatch = {k: ContextWrap(v) for k, v in Profile.dispatch.items()} + + +# run statements shamelessly stolen from profile.py +def run(statement, filename=None, sort=-1): + """Run statement under profiler optionally saving results in filename + + This function takes a single argument that can be passed to the + "exec" statement, and an optional file name. In all cases this + routine attempts to "exec" its first argument and gather profiling + statistics from the execution. If no file name is present, then this + function automatically prints a simple profiling report, sorted by the + standard name string (file/line/function-name) that is presented in + each line. + """ + prof = Profile() + try: + prof = prof.run(statement) + except SystemExit: + pass + if filename is not None: + prof.dump_stats(filename) + else: + return prof.print_stats(sort) + + +def runctx(statement, globals, locals, filename=None): + """Run statement under profiler, supplying your own globals and locals, + optionally saving results in filename. + + statement and filename have the same semantics as profile.run + """ + prof = Profile() + try: + prof = prof.runctx(statement, globals, locals) + except SystemExit: + pass + + if filename is not None: + prof.dump_stats(filename) + else: + return prof.print_stats() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/select.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/select.py new file mode 100644 index 0000000..a87d10d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/select.py @@ -0,0 +1,86 @@ +import eventlet +from eventlet.hubs import get_hub +__select = eventlet.patcher.original('select') +error = __select.error + + +__patched__ = ['select'] +__deleted__ = ['devpoll', 'poll', 'epoll', 'kqueue', 'kevent'] + + +def get_fileno(obj): + # The purpose of this function is to exactly replicate + # the behavior of the select module when confronted with + # abnormal filenos; the details are extensively tested in + # the stdlib test/test_select.py. + try: + f = obj.fileno + except AttributeError: + if not isinstance(obj, int): + raise TypeError("Expected int or long, got %s" % type(obj)) + return obj + else: + rv = f() + if not isinstance(rv, int): + raise TypeError("Expected int or long, got %s" % type(rv)) + return rv + + +def select(read_list, write_list, error_list, timeout=None): + # error checking like this is required by the stdlib unit tests + if timeout is not None: + try: + timeout = float(timeout) + except ValueError: + raise TypeError("Expected number for timeout") + hub = get_hub() + timers = [] + current = eventlet.getcurrent() + if hub.greenlet is current: + raise RuntimeError('do not call blocking functions from the mainloop') + ds = {} + for r in read_list: + ds[get_fileno(r)] = {'read': r} + for w in write_list: + ds.setdefault(get_fileno(w), {})['write'] = w + for e in error_list: + ds.setdefault(get_fileno(e), {})['error'] = e + + listeners = [] + + def on_read(d): + original = ds[get_fileno(d)]['read'] + current.switch(([original], [], [])) + + def on_write(d): + original = ds[get_fileno(d)]['write'] + current.switch(([], [original], [])) + + def on_timeout2(): + current.switch(([], [], [])) + + def on_timeout(): + # ensure that BaseHub.run() has a chance to call self.wait() + # at least once before timed out. otherwise the following code + # can time out erroneously. + # + # s1, s2 = socket.socketpair() + # print(select.select([], [s1], [], 0)) + timers.append(hub.schedule_call_global(0, on_timeout2)) + + if timeout is not None: + timers.append(hub.schedule_call_global(timeout, on_timeout)) + try: + for k, v in ds.items(): + if v.get('read'): + listeners.append(hub.add(hub.READ, k, on_read, current.throw, lambda: None)) + if v.get('write'): + listeners.append(hub.add(hub.WRITE, k, on_write, current.throw, lambda: None)) + try: + return hub.switch() + finally: + for l in listeners: + hub.remove(l) + finally: + for t in timers: + t.cancel() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/selectors.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/selectors.py new file mode 100644 index 0000000..81fc862 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/selectors.py @@ -0,0 +1,34 @@ +import sys + +from eventlet import patcher +from eventlet.green import select + +__patched__ = [ + 'DefaultSelector', + 'SelectSelector', +] + +# We only have green select so the options are: +# * leave it be and have selectors that block +# * try to pretend the "bad" selectors don't exist +# * replace all with SelectSelector for the price of possibly different +# performance characteristic and missing fileno() method (if someone +# uses it it'll result in a crash, we may want to implement it in the future) +# +# This module used to follow the third approach but just removing the offending +# selectors is less error prone and less confusing approach. +__deleted__ = [ + 'PollSelector', + 'EpollSelector', + 'DevpollSelector', + 'KqueueSelector', +] + +patcher.inject('selectors', globals(), ('select', select)) + +del patcher + +if sys.platform != 'win32': + SelectSelector._select = staticmethod(select.select) + +DefaultSelector = SelectSelector diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/socket.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/socket.py new file mode 100644 index 0000000..6a39caf --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/socket.py @@ -0,0 +1,63 @@ +import os +import sys + +__import__('eventlet.green._socket_nodns') +__socket = sys.modules['eventlet.green._socket_nodns'] + +__all__ = __socket.__all__ +__patched__ = __socket.__patched__ + [ + 'create_connection', + 'getaddrinfo', + 'gethostbyname', + 'gethostbyname_ex', + 'getnameinfo', +] + +from eventlet.patcher import slurp_properties +slurp_properties(__socket, globals(), srckeys=dir(__socket)) + + +if os.environ.get("EVENTLET_NO_GREENDNS", '').lower() != 'yes': + from eventlet.support import greendns + gethostbyname = greendns.gethostbyname + getaddrinfo = greendns.getaddrinfo + gethostbyname_ex = greendns.gethostbyname_ex + getnameinfo = greendns.getnameinfo + del greendns + + +def create_connection(address, + timeout=_GLOBAL_DEFAULT_TIMEOUT, + source_address=None): + """Connect to *address* and return the socket object. + + Convenience function. Connect to *address* (a 2-tuple ``(host, + port)``) and return the socket object. Passing the optional + *timeout* parameter will set the timeout on the socket instance + before attempting to connect. If no *timeout* is supplied, the + global default timeout setting returned by :func:`getdefaulttimeout` + is used. + """ + + err = "getaddrinfo returns an empty list" + host, port = address + for res in getaddrinfo(host, port, 0, SOCK_STREAM): + af, socktype, proto, canonname, sa = res + sock = None + try: + sock = socket(af, socktype, proto) + if timeout is not _GLOBAL_DEFAULT_TIMEOUT: + sock.settimeout(timeout) + if source_address: + sock.bind(source_address) + sock.connect(sa) + return sock + + except error as e: + err = e + if sock is not None: + sock.close() + + if not isinstance(err, error): + err = error(err) + raise err diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/ssl.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/ssl.py new file mode 100644 index 0000000..7ceb3c7 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/ssl.py @@ -0,0 +1,487 @@ +__ssl = __import__('ssl') + +from eventlet.patcher import slurp_properties +slurp_properties(__ssl, globals(), srckeys=dir(__ssl)) + +import sys +from eventlet import greenio, hubs +from eventlet.greenio import ( + GreenSocket, CONNECT_ERR, CONNECT_SUCCESS, +) +from eventlet.hubs import trampoline, IOClosed +from eventlet.support import get_errno, PY33 +from contextlib import contextmanager + +orig_socket = __import__('socket') +socket = orig_socket.socket +timeout_exc = orig_socket.timeout + +__patched__ = [ + 'SSLSocket', 'SSLContext', 'wrap_socket', 'sslwrap_simple', + 'create_default_context', '_create_default_https_context'] + +_original_sslsocket = __ssl.SSLSocket +_original_sslcontext = __ssl.SSLContext +_is_py_3_7 = sys.version_info[:2] == (3, 7) +_original_wrap_socket = __ssl.SSLContext.wrap_socket + + +@contextmanager +def _original_ssl_context(*args, **kwargs): + tmp_sslcontext = _original_wrap_socket.__globals__.get('SSLContext', None) + tmp_sslsocket = _original_sslsocket._create.__globals__.get('SSLSocket', None) + _original_sslsocket._create.__globals__['SSLSocket'] = _original_sslsocket + _original_wrap_socket.__globals__['SSLContext'] = _original_sslcontext + try: + yield + finally: + _original_wrap_socket.__globals__['SSLContext'] = tmp_sslcontext + _original_sslsocket._create.__globals__['SSLSocket'] = tmp_sslsocket + + +class GreenSSLSocket(_original_sslsocket): + """ This is a green version of the SSLSocket class from the ssl module added + in 2.6. For documentation on it, please see the Python standard + documentation. + + Python nonblocking ssl objects don't give errors when the other end + of the socket is closed (they do notice when the other end is shutdown, + though). Any write/read operations will simply hang if the socket is + closed from the other end. There is no obvious fix for this problem; + it appears to be a limitation of Python's ssl object implementation. + A workaround is to set a reasonable timeout on the socket using + settimeout(), and to close/reopen the connection when a timeout + occurs at an unexpected juncture in the code. + """ + def __new__(cls, sock=None, keyfile=None, certfile=None, + server_side=False, cert_reqs=CERT_NONE, + ssl_version=PROTOCOL_TLS, ca_certs=None, + do_handshake_on_connect=True, *args, **kw): + if not isinstance(sock, GreenSocket): + sock = GreenSocket(sock) + with _original_ssl_context(): + context = kw.get('_context') + if context: + ret = _original_sslsocket._create( + sock=sock.fd, + server_side=server_side, + do_handshake_on_connect=False, + suppress_ragged_eofs=kw.get('suppress_ragged_eofs', True), + server_hostname=kw.get('server_hostname'), + context=context, + session=kw.get('session'), + ) + else: + ret = cls._wrap_socket( + sock=sock.fd, + keyfile=keyfile, + certfile=certfile, + server_side=server_side, + cert_reqs=cert_reqs, + ssl_version=ssl_version, + ca_certs=ca_certs, + do_handshake_on_connect=False, + ciphers=kw.get('ciphers'), + ) + ret.keyfile = keyfile + ret.certfile = certfile + ret.cert_reqs = cert_reqs + ret.ssl_version = ssl_version + ret.ca_certs = ca_certs + ret.__class__ = GreenSSLSocket + return ret + + @staticmethod + def _wrap_socket(sock, keyfile, certfile, server_side, cert_reqs, + ssl_version, ca_certs, do_handshake_on_connect, ciphers): + context = _original_sslcontext(protocol=ssl_version) + context.options |= cert_reqs + if certfile or keyfile: + context.load_cert_chain( + certfile=certfile, + keyfile=keyfile, + ) + if ca_certs: + context.load_verify_locations(ca_certs) + if ciphers: + context.set_ciphers(ciphers) + return context.wrap_socket( + sock=sock, + server_side=server_side, + do_handshake_on_connect=do_handshake_on_connect, + ) + + # we are inheriting from SSLSocket because its constructor calls + # do_handshake whose behavior we wish to override + def __init__(self, sock, keyfile=None, certfile=None, + server_side=False, cert_reqs=CERT_NONE, + ssl_version=PROTOCOL_TLS, ca_certs=None, + do_handshake_on_connect=True, *args, **kw): + if not isinstance(sock, GreenSocket): + sock = GreenSocket(sock) + self.act_non_blocking = sock.act_non_blocking + + # the superclass initializer trashes the methods so we remove + # the local-object versions of them and let the actual class + # methods shine through + # Note: This for Python 2 + try: + for fn in orig_socket._delegate_methods: + delattr(self, fn) + except AttributeError: + pass + + # Python 3 SSLSocket construction process overwrites the timeout so restore it + self._timeout = sock.gettimeout() + + # it also sets timeout to None internally apparently (tested with 3.4.2) + _original_sslsocket.settimeout(self, 0.0) + assert _original_sslsocket.gettimeout(self) == 0.0 + + # see note above about handshaking + self.do_handshake_on_connect = do_handshake_on_connect + if do_handshake_on_connect and self._connected: + self.do_handshake() + + def settimeout(self, timeout): + self._timeout = timeout + + def gettimeout(self): + return self._timeout + + def setblocking(self, flag): + if flag: + self.act_non_blocking = False + self._timeout = None + else: + self.act_non_blocking = True + self._timeout = 0.0 + + def _call_trampolining(self, func, *a, **kw): + if self.act_non_blocking: + return func(*a, **kw) + else: + while True: + try: + return func(*a, **kw) + except SSLError as exc: + if get_errno(exc) == SSL_ERROR_WANT_READ: + trampoline(self, + read=True, + timeout=self.gettimeout(), + timeout_exc=timeout_exc('timed out')) + elif get_errno(exc) == SSL_ERROR_WANT_WRITE: + trampoline(self, + write=True, + timeout=self.gettimeout(), + timeout_exc=timeout_exc('timed out')) + elif _is_py_3_7 and "unexpected eof" in exc.args[1]: + # For reasons I don't understand on 3.7 we get [ssl: + # KRB5_S_TKT_NYV] unexpected eof while reading] + # errors... + raise IOClosed + else: + raise + + def write(self, data): + """Write DATA to the underlying SSL channel. Returns + number of bytes of DATA actually transmitted.""" + return self._call_trampolining( + super().write, data) + + def read(self, len=1024, buffer=None): + """Read up to LEN bytes and return them. + Return zero-length string on EOF.""" + try: + return self._call_trampolining( + super().read, len, buffer) + except IOClosed: + if buffer is None: + return b'' + else: + return 0 + + def send(self, data, flags=0): + if self._sslobj: + return self._call_trampolining( + super().send, data, flags) + else: + trampoline(self, write=True, timeout_exc=timeout_exc('timed out')) + return socket.send(self, data, flags) + + def sendto(self, data, addr, flags=0): + # *NOTE: gross, copied code from ssl.py becase it's not factored well enough to be used as-is + if self._sslobj: + raise ValueError("sendto not allowed on instances of %s" % + self.__class__) + else: + trampoline(self, write=True, timeout_exc=timeout_exc('timed out')) + return socket.sendto(self, data, addr, flags) + + def sendall(self, data, flags=0): + # *NOTE: gross, copied code from ssl.py becase it's not factored well enough to be used as-is + if self._sslobj: + if flags != 0: + raise ValueError( + "non-zero flags not allowed in calls to sendall() on %s" % + self.__class__) + amount = len(data) + count = 0 + data_to_send = data + while (count < amount): + v = self.send(data_to_send) + count += v + if v == 0: + trampoline(self, write=True, timeout_exc=timeout_exc('timed out')) + else: + data_to_send = data[count:] + return amount + else: + while True: + try: + return socket.sendall(self, data, flags) + except orig_socket.error as e: + if self.act_non_blocking: + raise + erno = get_errno(e) + if erno in greenio.SOCKET_BLOCKING: + trampoline(self, write=True, + timeout=self.gettimeout(), timeout_exc=timeout_exc('timed out')) + elif erno in greenio.SOCKET_CLOSED: + return '' + raise + + def recv(self, buflen=1024, flags=0): + return self._base_recv(buflen, flags, into=False) + + def recv_into(self, buffer, nbytes=None, flags=0): + # Copied verbatim from CPython + if buffer and nbytes is None: + nbytes = len(buffer) + elif nbytes is None: + nbytes = 1024 + # end of CPython code + + return self._base_recv(nbytes, flags, into=True, buffer_=buffer) + + def _base_recv(self, nbytes, flags, into, buffer_=None): + if into: + plain_socket_function = socket.recv_into + else: + plain_socket_function = socket.recv + + # *NOTE: gross, copied code from ssl.py becase it's not factored well enough to be used as-is + if self._sslobj: + if flags != 0: + raise ValueError( + "non-zero flags not allowed in calls to %s() on %s" % + plain_socket_function.__name__, self.__class__) + if into: + read = self.read(nbytes, buffer_) + else: + read = self.read(nbytes) + return read + else: + while True: + try: + args = [self, nbytes, flags] + if into: + args.insert(1, buffer_) + return plain_socket_function(*args) + except orig_socket.error as e: + if self.act_non_blocking: + raise + erno = get_errno(e) + if erno in greenio.SOCKET_BLOCKING: + try: + trampoline( + self, read=True, + timeout=self.gettimeout(), timeout_exc=timeout_exc('timed out')) + except IOClosed: + return b'' + elif erno in greenio.SOCKET_CLOSED: + return b'' + raise + + def recvfrom(self, addr, buflen=1024, flags=0): + if not self.act_non_blocking: + trampoline(self, read=True, timeout=self.gettimeout(), + timeout_exc=timeout_exc('timed out')) + return super().recvfrom(addr, buflen, flags) + + def recvfrom_into(self, buffer, nbytes=None, flags=0): + if not self.act_non_blocking: + trampoline(self, read=True, timeout=self.gettimeout(), + timeout_exc=timeout_exc('timed out')) + return super().recvfrom_into(buffer, nbytes, flags) + + def unwrap(self): + return GreenSocket(self._call_trampolining( + super().unwrap)) + + def do_handshake(self): + """Perform a TLS/SSL handshake.""" + return self._call_trampolining( + super().do_handshake) + + def _socket_connect(self, addr): + real_connect = socket.connect + if self.act_non_blocking: + return real_connect(self, addr) + else: + clock = hubs.get_hub().clock + # *NOTE: gross, copied code from greenio because it's not factored + # well enough to reuse + if self.gettimeout() is None: + while True: + try: + return real_connect(self, addr) + except orig_socket.error as exc: + if get_errno(exc) in CONNECT_ERR: + trampoline(self, write=True) + elif get_errno(exc) in CONNECT_SUCCESS: + return + else: + raise + else: + end = clock() + self.gettimeout() + while True: + try: + real_connect(self, addr) + except orig_socket.error as exc: + if get_errno(exc) in CONNECT_ERR: + trampoline( + self, write=True, + timeout=end - clock(), timeout_exc=timeout_exc('timed out')) + elif get_errno(exc) in CONNECT_SUCCESS: + return + else: + raise + if clock() >= end: + raise timeout_exc('timed out') + + def connect(self, addr): + """Connects to remote ADDR, and then wraps the connection in + an SSL channel.""" + # *NOTE: grrrrr copied this code from ssl.py because of the reference + # to socket.connect which we don't want to call directly + if self._sslobj: + raise ValueError("attempt to connect already-connected SSLSocket!") + self._socket_connect(addr) + server_side = False + try: + sslwrap = _ssl.sslwrap + except AttributeError: + # sslwrap was removed in 3.x and later in 2.7.9 + context = self.context if PY33 else self._context + sslobj = context._wrap_socket(self, server_side, server_hostname=self.server_hostname) + else: + sslobj = sslwrap(self._sock, server_side, self.keyfile, self.certfile, + self.cert_reqs, self.ssl_version, + self.ca_certs, *self.ciphers) + + try: + # This is added in Python 3.5, http://bugs.python.org/issue21965 + SSLObject + except NameError: + self._sslobj = sslobj + else: + self._sslobj = sslobj + + if self.do_handshake_on_connect: + self.do_handshake() + + def accept(self): + """Accepts a new connection from a remote client, and returns + a tuple containing that new connection wrapped with a server-side + SSL channel, and the address of the remote client.""" + # RDW grr duplication of code from greenio + if self.act_non_blocking: + newsock, addr = socket.accept(self) + else: + while True: + try: + newsock, addr = socket.accept(self) + break + except orig_socket.error as e: + if get_errno(e) not in greenio.SOCKET_BLOCKING: + raise + trampoline(self, read=True, timeout=self.gettimeout(), + timeout_exc=timeout_exc('timed out')) + + new_ssl = type(self)( + newsock, + server_side=True, + do_handshake_on_connect=False, + suppress_ragged_eofs=self.suppress_ragged_eofs, + _context=self._context, + ) + return (new_ssl, addr) + + def dup(self): + raise NotImplementedError("Can't dup an ssl object") + + +SSLSocket = GreenSSLSocket + + +def wrap_socket(sock, *a, **kw): + return GreenSSLSocket(sock, *a, **kw) + + +class GreenSSLContext(_original_sslcontext): + __slots__ = () + + def wrap_socket(self, sock, *a, **kw): + return GreenSSLSocket(sock, *a, _context=self, **kw) + + # https://github.com/eventlet/eventlet/issues/371 + # Thanks to Gevent developers for sharing patch to this problem. + if hasattr(_original_sslcontext.options, 'setter'): + # In 3.6, these became properties. They want to access the + # property __set__ method in the superclass, and they do so by using + # super(SSLContext, SSLContext). But we rebind SSLContext when we monkey + # patch, which causes infinite recursion. + # https://github.com/python/cpython/commit/328067c468f82e4ec1b5c510a4e84509e010f296 + @_original_sslcontext.options.setter + def options(self, value): + super(_original_sslcontext, _original_sslcontext).options.__set__(self, value) + + @_original_sslcontext.verify_flags.setter + def verify_flags(self, value): + super(_original_sslcontext, _original_sslcontext).verify_flags.__set__(self, value) + + @_original_sslcontext.verify_mode.setter + def verify_mode(self, value): + super(_original_sslcontext, _original_sslcontext).verify_mode.__set__(self, value) + + if hasattr(_original_sslcontext, "maximum_version"): + @_original_sslcontext.maximum_version.setter + def maximum_version(self, value): + super(_original_sslcontext, _original_sslcontext).maximum_version.__set__(self, value) + + if hasattr(_original_sslcontext, "minimum_version"): + @_original_sslcontext.minimum_version.setter + def minimum_version(self, value): + super(_original_sslcontext, _original_sslcontext).minimum_version.__set__(self, value) + + +SSLContext = GreenSSLContext + + +# TODO: ssl.create_default_context() was added in 2.7.9. +# Not clear we're still trying to support Python versions even older than that. +if hasattr(__ssl, 'create_default_context'): + _original_create_default_context = __ssl.create_default_context + + def green_create_default_context(*a, **kw): + # We can't just monkey-patch on the green version of `wrap_socket` + # on to SSLContext instances, but SSLContext.create_default_context + # does a bunch of work. Rather than re-implementing it all, just + # switch out the __class__ to get our `wrap_socket` implementation + context = _original_create_default_context(*a, **kw) + context.__class__ = GreenSSLContext + return context + + create_default_context = green_create_default_context + _create_default_https_context = green_create_default_context diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/subprocess.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/subprocess.py new file mode 100644 index 0000000..4509208 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/subprocess.py @@ -0,0 +1,137 @@ +import errno +import sys +from types import FunctionType + +import eventlet +from eventlet import greenio +from eventlet import patcher +from eventlet.green import select, threading, time + + +__patched__ = ['call', 'check_call', 'Popen'] +to_patch = [('select', select), ('threading', threading), ('time', time)] + +from eventlet.green import selectors +to_patch.append(('selectors', selectors)) + +patcher.inject('subprocess', globals(), *to_patch) +subprocess_orig = patcher.original("subprocess") +subprocess_imported = sys.modules.get('subprocess', subprocess_orig) +mswindows = sys.platform == "win32" + + +if getattr(subprocess_orig, 'TimeoutExpired', None) is None: + # Backported from Python 3.3. + # https://bitbucket.org/eventlet/eventlet/issue/89 + class TimeoutExpired(Exception): + """This exception is raised when the timeout expires while waiting for + a child process. + """ + + def __init__(self, cmd, timeout, output=None): + self.cmd = cmd + self.timeout = timeout + self.output = output + + def __str__(self): + return ("Command '%s' timed out after %s seconds" % + (self.cmd, self.timeout)) +else: + TimeoutExpired = subprocess_imported.TimeoutExpired + + +# This is the meat of this module, the green version of Popen. +class Popen(subprocess_orig.Popen): + """eventlet-friendly version of subprocess.Popen""" + # We do not believe that Windows pipes support non-blocking I/O. At least, + # the Python file objects stored on our base-class object have no + # setblocking() method, and the Python fcntl module doesn't exist on + # Windows. (see eventlet.greenio.set_nonblocking()) As the sole purpose of + # this __init__() override is to wrap the pipes for eventlet-friendly + # non-blocking I/O, don't even bother overriding it on Windows. + if not mswindows: + def __init__(self, args, bufsize=0, *argss, **kwds): + self.args = args + # Forward the call to base-class constructor + subprocess_orig.Popen.__init__(self, args, 0, *argss, **kwds) + # Now wrap the pipes, if any. This logic is loosely borrowed from + # eventlet.processes.Process.run() method. + for attr in "stdin", "stdout", "stderr": + pipe = getattr(self, attr) + if pipe is not None and type(pipe) != greenio.GreenPipe: + # https://github.com/eventlet/eventlet/issues/243 + # AttributeError: '_io.TextIOWrapper' object has no attribute 'mode' + mode = getattr(pipe, 'mode', '') + if not mode: + if pipe.readable(): + mode += 'r' + if pipe.writable(): + mode += 'w' + # ValueError: can't have unbuffered text I/O + if bufsize == 0: + bufsize = -1 + wrapped_pipe = greenio.GreenPipe(pipe, mode, bufsize) + setattr(self, attr, wrapped_pipe) + __init__.__doc__ = subprocess_orig.Popen.__init__.__doc__ + + def wait(self, timeout=None, check_interval=0.01): + # Instead of a blocking OS call, this version of wait() uses logic + # borrowed from the eventlet 0.2 processes.Process.wait() method. + if timeout is not None: + endtime = time.time() + timeout + try: + while True: + status = self.poll() + if status is not None: + return status + if timeout is not None and time.time() > endtime: + raise TimeoutExpired(self.args, timeout) + eventlet.sleep(check_interval) + except OSError as e: + if e.errno == errno.ECHILD: + # no child process, this happens if the child process + # already died and has been cleaned up + return -1 + else: + raise + wait.__doc__ = subprocess_orig.Popen.wait.__doc__ + + if not mswindows: + # don't want to rewrite the original _communicate() method, we + # just want a version that uses eventlet.green.select.select() + # instead of select.select(). + _communicate = FunctionType( + subprocess_orig.Popen._communicate.__code__, + globals()) + try: + _communicate_with_select = FunctionType( + subprocess_orig.Popen._communicate_with_select.__code__, + globals()) + _communicate_with_poll = FunctionType( + subprocess_orig.Popen._communicate_with_poll.__code__, + globals()) + except AttributeError: + pass + + +# Borrow subprocess.call() and check_call(), but patch them so they reference +# OUR Popen class rather than subprocess.Popen. +def patched_function(function): + new_function = FunctionType(function.__code__, globals()) + new_function.__kwdefaults__ = function.__kwdefaults__ + new_function.__defaults__ = function.__defaults__ + return new_function + + +call = patched_function(subprocess_orig.call) +check_call = patched_function(subprocess_orig.check_call) +# check_output is Python 2.7+ +if hasattr(subprocess_orig, 'check_output'): + __patched__.append('check_output') + check_output = patched_function(subprocess_orig.check_output) +del patched_function + +# Keep exceptions identity. +# https://github.com/eventlet/eventlet/issues/413 +CalledProcessError = subprocess_imported.CalledProcessError +del subprocess_imported diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/thread.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/thread.py new file mode 100644 index 0000000..224cd1c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/thread.py @@ -0,0 +1,178 @@ +"""Implements the standard thread module, using greenthreads.""" +import _thread as __thread +from eventlet.support import greenlets as greenlet +from eventlet import greenthread +from eventlet.timeout import with_timeout +from eventlet.lock import Lock +import sys + + +__patched__ = ['Lock', 'LockType', '_ThreadHandle', '_count', + '_get_main_thread_ident', '_local', '_make_thread_handle', + 'allocate', 'allocate_lock', 'exit', 'get_ident', + 'interrupt_main', 'stack_size', 'start_joinable_thread', + 'start_new', 'start_new_thread'] + +error = __thread.error +LockType = Lock +__threadcount = 0 + +if hasattr(__thread, "_is_main_interpreter"): + _is_main_interpreter = __thread._is_main_interpreter + + +def _set_sentinel(): + # TODO this is a dummy code, reimplementing this may be needed: + # https://hg.python.org/cpython/file/b5e9bc4352e1/Modules/_threadmodule.c#l1203 + return allocate_lock() + + +TIMEOUT_MAX = __thread.TIMEOUT_MAX + + +def _count(): + return __threadcount + + +def get_ident(gr=None): + if gr is None: + return id(greenlet.getcurrent()) + else: + return id(gr) + + +def __thread_body(func, args, kwargs): + global __threadcount + __threadcount += 1 + try: + func(*args, **kwargs) + finally: + __threadcount -= 1 + + +class _ThreadHandle: + def __init__(self, greenthread=None): + self._greenthread = greenthread + self._done = False + + def _set_done(self): + self._done = True + + def is_done(self): + if self._greenthread is not None: + return self._greenthread.dead + return self._done + + @property + def ident(self): + return get_ident(self._greenthread) + + def join(self, timeout=None): + if not hasattr(self._greenthread, "wait"): + return + if timeout is not None: + return with_timeout(timeout, self._greenthread.wait) + return self._greenthread.wait() + + +def _make_thread_handle(ident): + greenthread = greenlet.getcurrent() + assert ident == get_ident(greenthread) + return _ThreadHandle(greenthread=greenthread) + + +def __spawn_green(function, args=(), kwargs=None, joinable=False): + if ((3, 4) <= sys.version_info < (3, 13) + and getattr(function, '__module__', '') == 'threading' + and hasattr(function, '__self__')): + # In Python 3.4-3.12, threading.Thread uses an internal lock + # automatically released when the python thread state is deleted. + # With monkey patching, eventlet uses green threads without python + # thread state, so the lock is not automatically released. + # + # Wrap _bootstrap_inner() to release explicitly the thread state lock + # when the thread completes. + thread = function.__self__ + bootstrap_inner = thread._bootstrap_inner + + def wrap_bootstrap_inner(): + try: + bootstrap_inner() + finally: + # The lock can be cleared (ex: by a fork()) + if getattr(thread, "_tstate_lock", None) is not None: + thread._tstate_lock.release() + + thread._bootstrap_inner = wrap_bootstrap_inner + + kwargs = kwargs or {} + spawn_func = greenthread.spawn if joinable else greenthread.spawn_n + return spawn_func(__thread_body, function, args, kwargs) + + +def start_joinable_thread(function, handle=None, daemon=True): + g = __spawn_green(function, joinable=True) + if handle is None: + handle = _ThreadHandle(greenthread=g) + else: + handle._greenthread = g + return handle + + +def start_new_thread(function, args=(), kwargs=None): + g = __spawn_green(function, args=args, kwargs=kwargs) + return get_ident(g) + + +start_new = start_new_thread + + +def _get_main_thread_ident(): + greenthread = greenlet.getcurrent() + while greenthread.parent is not None: + greenthread = greenthread.parent + return get_ident(greenthread) + + +def allocate_lock(*a): + return LockType(1) + + +allocate = allocate_lock + + +def exit(): + raise greenlet.GreenletExit + + +exit_thread = __thread.exit_thread + + +def interrupt_main(): + curr = greenlet.getcurrent() + if curr.parent and not curr.parent.dead: + curr.parent.throw(KeyboardInterrupt()) + else: + raise KeyboardInterrupt() + + +if hasattr(__thread, 'stack_size'): + __original_stack_size__ = __thread.stack_size + + def stack_size(size=None): + if size is None: + return __original_stack_size__() + if size > __original_stack_size__(): + return __original_stack_size__(size) + else: + pass + # not going to decrease stack_size, because otherwise other greenlets in + # this thread will suffer + +from eventlet.corolocal import local as _local + +if hasattr(__thread, 'daemon_threads_allowed'): + daemon_threads_allowed = __thread.daemon_threads_allowed + +if hasattr(__thread, '_shutdown'): + _shutdown = __thread._shutdown diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/threading.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/threading.py new file mode 100644 index 0000000..ae01a5b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/threading.py @@ -0,0 +1,133 @@ +"""Implements the standard threading module, using greenthreads.""" +import eventlet +from eventlet.green import thread +from eventlet.green import time +from eventlet.support import greenlets as greenlet + +__patched__ = ['Lock', '_allocate_lock', '_get_main_thread_ident', + '_make_thread_handle', '_shutdown', '_sleep', + '_start_joinable_thread', '_start_new_thread', '_ThreadHandle', + 'currentThread', 'current_thread', 'local', 'stack_size', + "_active", "_limbo"] + +__patched__ += ['get_ident', '_set_sentinel'] + +__orig_threading = eventlet.patcher.original('threading') +__threadlocal = __orig_threading.local() +__patched_enumerate = None + + +eventlet.patcher.inject( + 'threading', + globals(), + ('_thread', thread), + ('time', time)) + + +_count = 1 + + +class _GreenThread: + """Wrapper for GreenThread objects to provide Thread-like attributes + and methods""" + + def __init__(self, g): + global _count + self._g = g + self._name = 'GreenThread-%d' % _count + _count += 1 + + def __repr__(self): + return '<_GreenThread(%s, %r)>' % (self._name, self._g) + + def join(self, timeout=None): + return self._g.wait() + + def getName(self): + return self._name + get_name = getName + + def setName(self, name): + self._name = str(name) + set_name = setName + + name = property(getName, setName) + + ident = property(lambda self: id(self._g)) + + def isAlive(self): + return True + is_alive = isAlive + + daemon = property(lambda self: True) + + def isDaemon(self): + return self.daemon + is_daemon = isDaemon + + +__threading = None + + +def _fixup_thread(t): + # Some third-party packages (lockfile) will try to patch the + # threading.Thread class with a get_name attribute if it doesn't + # exist. Since we might return Thread objects from the original + # threading package that won't get patched, let's make sure each + # individual object gets patched too our patched threading.Thread + # class has been patched. This is why monkey patching can be bad... + global __threading + if not __threading: + __threading = __import__('threading') + + if (hasattr(__threading.Thread, 'get_name') and + not hasattr(t, 'get_name')): + t.get_name = t.getName + return t + + +def current_thread(): + global __patched_enumerate + g = greenlet.getcurrent() + if not g: + # Not currently in a greenthread, fall back to standard function + return _fixup_thread(__orig_threading.current_thread()) + + try: + active = __threadlocal.active + except AttributeError: + active = __threadlocal.active = {} + + g_id = id(g) + t = active.get(g_id) + if t is not None: + return t + + # FIXME: move import from function body to top + # (jaketesler@github) Furthermore, I was unable to have the current_thread() return correct results from + # threading.enumerate() unless the enumerate() function was a) imported at runtime using the gross __import__() call + # and b) was hot-patched using patch_function(). + # https://github.com/eventlet/eventlet/issues/172#issuecomment-379421165 + if __patched_enumerate is None: + __patched_enumerate = eventlet.patcher.patch_function(__import__('threading').enumerate) + found = [th for th in __patched_enumerate() if th.ident == g_id] + if found: + return found[0] + + # Add green thread to active if we can clean it up on exit + def cleanup(g): + del active[g_id] + try: + g.link(cleanup) + except AttributeError: + # Not a GreenThread type, so there's no way to hook into + # the green thread exiting. Fall back to the standard + # function then. + t = _fixup_thread(__orig_threading.current_thread()) + else: + t = active[g_id] = _GreenThread(g) + + return t + + +currentThread = current_thread diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/time.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/time.py new file mode 100644 index 0000000..0fbe30e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/time.py @@ -0,0 +1,6 @@ +__time = __import__('time') +from eventlet.patcher import slurp_properties +__patched__ = ['sleep'] +slurp_properties(__time, globals(), ignore=__patched__, srckeys=dir(__time)) +from eventlet.greenthread import sleep +sleep # silence pyflakes diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/__init__.py new file mode 100644 index 0000000..44335dd --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/__init__.py @@ -0,0 +1,5 @@ +from eventlet import patcher +from eventlet.green import socket +from eventlet.green import time +from eventlet.green import httplib +from eventlet.green import ftplib diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/error.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/error.py new file mode 100644 index 0000000..6913813 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/error.py @@ -0,0 +1,4 @@ +from eventlet import patcher +from eventlet.green.urllib import response +patcher.inject('urllib.error', globals(), ('urllib.response', response)) +del patcher diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/parse.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/parse.py new file mode 100644 index 0000000..f3a8924 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/parse.py @@ -0,0 +1,3 @@ +from eventlet import patcher +patcher.inject('urllib.parse', globals()) +del patcher diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/request.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/request.py new file mode 100644 index 0000000..43c198e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/request.py @@ -0,0 +1,57 @@ +import sys + +from eventlet import patcher +from eventlet.green import ftplib, http, os, socket, time +from eventlet.green.http import client as http_client +from eventlet.green.urllib import error, parse, response + +# TODO should we also have green email version? +# import email + + +to_patch = [ + # This (http module) is needed here, otherwise test__greenness hangs + # forever on Python 3 because parts of non-green http (including + # http.client) leak into our patched urllib.request. There may be a nicer + # way to handle this (I didn't dig too deep) but this does the job. Jakub + ('http', http), + + ('http.client', http_client), + ('os', os), + ('socket', socket), + ('time', time), + ('urllib.error', error), + ('urllib.parse', parse), + ('urllib.response', response), +] + +try: + from eventlet.green import ssl +except ImportError: + pass +else: + to_patch.append(('ssl', ssl)) + +patcher.inject('urllib.request', globals(), *to_patch) +del to_patch + +to_patch_in_functions = [('ftplib', ftplib)] +del ftplib + +FTPHandler.ftp_open = patcher.patch_function(FTPHandler.ftp_open, *to_patch_in_functions) + +if sys.version_info < (3, 14): + URLopener.open_ftp = patcher.patch_function(URLopener.open_ftp, *to_patch_in_functions) +else: + # Removed in python3.14+, nothing to do + pass + +ftperrors = patcher.patch_function(ftperrors, *to_patch_in_functions) + +ftpwrapper.init = patcher.patch_function(ftpwrapper.init, *to_patch_in_functions) +ftpwrapper.retrfile = patcher.patch_function(ftpwrapper.retrfile, *to_patch_in_functions) + +del error +del parse +del response +del to_patch_in_functions diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/response.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/response.py new file mode 100644 index 0000000..f9aaba5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib/response.py @@ -0,0 +1,3 @@ +from eventlet import patcher +patcher.inject('urllib.response', globals()) +del patcher diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib2.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib2.py new file mode 100644 index 0000000..c53ecbb --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/urllib2.py @@ -0,0 +1,20 @@ +from eventlet import patcher +from eventlet.green import ftplib +from eventlet.green import httplib +from eventlet.green import socket +from eventlet.green import ssl +from eventlet.green import time +from eventlet.green import urllib + +patcher.inject( + 'urllib2', + globals(), + ('httplib', httplib), + ('socket', socket), + ('ssl', ssl), + ('time', time), + ('urllib', urllib)) + +FTPHandler.ftp_open = patcher.patch_function(FTPHandler.ftp_open, ('ftplib', ftplib)) + +del patcher diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/green/zmq.py b/netdeploy/lib/python3.11/site-packages/eventlet/green/zmq.py new file mode 100644 index 0000000..865ee13 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/green/zmq.py @@ -0,0 +1,465 @@ +"""The :mod:`zmq` module wraps the :class:`Socket` and :class:`Context` +found in :mod:`pyzmq ` to be non blocking. +""" +__zmq__ = __import__('zmq') +import eventlet.hubs +from eventlet.patcher import slurp_properties +from eventlet.support import greenlets as greenlet + +__patched__ = ['Context', 'Socket'] +slurp_properties(__zmq__, globals(), ignore=__patched__) + +from collections import deque + +try: + # alias XREQ/XREP to DEALER/ROUTER if available + if not hasattr(__zmq__, 'XREQ'): + XREQ = DEALER + if not hasattr(__zmq__, 'XREP'): + XREP = ROUTER +except NameError: + pass + + +class LockReleaseError(Exception): + pass + + +class _QueueLock: + """A Lock that can be acquired by at most one thread. Any other + thread calling acquire will be blocked in a queue. When release + is called, the threads are awoken in the order they blocked, + one at a time. This lock can be required recursively by the same + thread.""" + + def __init__(self): + self._waiters = deque() + self._count = 0 + self._holder = None + self._hub = eventlet.hubs.get_hub() + + def __nonzero__(self): + return bool(self._count) + + __bool__ = __nonzero__ + + def __enter__(self): + self.acquire() + + def __exit__(self, type, value, traceback): + self.release() + + def acquire(self): + current = greenlet.getcurrent() + if (self._waiters or self._count > 0) and self._holder is not current: + # block until lock is free + self._waiters.append(current) + self._hub.switch() + w = self._waiters.popleft() + + assert w is current, 'Waiting threads woken out of order' + assert self._count == 0, 'After waking a thread, the lock must be unacquired' + + self._holder = current + self._count += 1 + + def release(self): + if self._count <= 0: + raise LockReleaseError("Cannot release unacquired lock") + + self._count -= 1 + if self._count == 0: + self._holder = None + if self._waiters: + # wake next + self._hub.schedule_call_global(0, self._waiters[0].switch) + + +class _BlockedThread: + """Is either empty, or represents a single blocked thread that + blocked itself by calling the block() method. The thread can be + awoken by calling wake(). Wake() can be called multiple times and + all but the first call will have no effect.""" + + def __init__(self): + self._blocked_thread = None + self._wakeupper = None + self._hub = eventlet.hubs.get_hub() + + def __nonzero__(self): + return self._blocked_thread is not None + + __bool__ = __nonzero__ + + def block(self, deadline=None): + if self._blocked_thread is not None: + raise Exception("Cannot block more than one thread on one BlockedThread") + self._blocked_thread = greenlet.getcurrent() + + if deadline is not None: + self._hub.schedule_call_local(deadline - self._hub.clock(), self.wake) + + try: + self._hub.switch() + finally: + self._blocked_thread = None + # cleanup the wakeup task + if self._wakeupper is not None: + # Important to cancel the wakeup task so it doesn't + # spuriously wake this greenthread later on. + self._wakeupper.cancel() + self._wakeupper = None + + def wake(self): + """Schedules the blocked thread to be awoken and return + True. If wake has already been called or if there is no + blocked thread, then this call has no effect and returns + False.""" + if self._blocked_thread is not None and self._wakeupper is None: + self._wakeupper = self._hub.schedule_call_global(0, self._blocked_thread.switch) + return True + return False + + +class Context(__zmq__.Context): + """Subclass of :class:`zmq.Context` + """ + + def socket(self, socket_type): + """Overridden method to ensure that the green version of socket is used + + Behaves the same as :meth:`zmq.Context.socket`, but ensures + that a :class:`Socket` with all of its send and recv methods set to be + non-blocking is returned + """ + if self.closed: + raise ZMQError(ENOTSUP) + return Socket(self, socket_type) + + +def _wraps(source_fn): + """A decorator that copies the __name__ and __doc__ from the given + function + """ + def wrapper(dest_fn): + dest_fn.__name__ = source_fn.__name__ + dest_fn.__doc__ = source_fn.__doc__ + return dest_fn + return wrapper + + +# Implementation notes: Each socket in 0mq contains a pipe that the +# background IO threads use to communicate with the socket. These +# events are important because they tell the socket when it is able to +# send and when it has messages waiting to be received. The read end +# of the events pipe is the same FD that getsockopt(zmq.FD) returns. +# +# Events are read from the socket's event pipe only on the thread that +# the 0mq context is associated with, which is the native thread the +# greenthreads are running on, and the only operations that cause the +# events to be read and processed are send(), recv() and +# getsockopt(zmq.EVENTS). This means that after doing any of these +# three operations, the ability of the socket to send or receive a +# message without blocking may have changed, but after the events are +# read the FD is no longer readable so the hub may not signal our +# listener. +# +# If we understand that after calling send() a message might be ready +# to be received and that after calling recv() a message might be able +# to be sent, what should we do next? There are two approaches: +# +# 1. Always wake the other thread if there is one waiting. This +# wakeup may be spurious because the socket might not actually be +# ready for a send() or recv(). However, if a thread is in a +# tight-loop successfully calling send() or recv() then the wakeups +# are naturally batched and there's very little cost added to each +# send/recv call. +# +# or +# +# 2. Call getsockopt(zmq.EVENTS) and explicitly check if the other +# thread should be woken up. This avoids spurious wake-ups but may +# add overhead because getsockopt will cause all events to be +# processed, whereas send and recv throttle processing +# events. Admittedly, all of the events will need to be processed +# eventually, but it is likely faster to batch the processing. +# +# Which approach is better? I have no idea. +# +# TODO: +# - Support MessageTrackers and make MessageTracker.wait green + +_Socket = __zmq__.Socket +_Socket_recv = _Socket.recv +_Socket_send = _Socket.send +_Socket_send_multipart = _Socket.send_multipart +_Socket_recv_multipart = _Socket.recv_multipart +_Socket_send_string = _Socket.send_string +_Socket_recv_string = _Socket.recv_string +_Socket_send_pyobj = _Socket.send_pyobj +_Socket_recv_pyobj = _Socket.recv_pyobj +_Socket_send_json = _Socket.send_json +_Socket_recv_json = _Socket.recv_json +_Socket_getsockopt = _Socket.getsockopt + + +class Socket(_Socket): + """Green version of :class:``zmq.core.socket.Socket``. + + The following three methods are always overridden: + * send + * recv + * getsockopt + To ensure that the ``zmq.NOBLOCK`` flag is set and that sending or receiving + is deferred to the hub (using :func:``eventlet.hubs.trampoline``) if a + ``zmq.EAGAIN`` (retry) error is raised. + + For some socket types, the following methods are also overridden: + * send_multipart + * recv_multipart + """ + + def __init__(self, context, socket_type): + super().__init__(context, socket_type) + + self.__dict__['_eventlet_send_event'] = _BlockedThread() + self.__dict__['_eventlet_recv_event'] = _BlockedThread() + self.__dict__['_eventlet_send_lock'] = _QueueLock() + self.__dict__['_eventlet_recv_lock'] = _QueueLock() + + def event(fd): + # Some events arrived at the zmq socket. This may mean + # there's a message that can be read or there's space for + # a message to be written. + send_wake = self._eventlet_send_event.wake() + recv_wake = self._eventlet_recv_event.wake() + if not send_wake and not recv_wake: + # if no waiting send or recv thread was woken up, then + # force the zmq socket's events to be processed to + # avoid repeated wakeups + _Socket_getsockopt(self, EVENTS) + + hub = eventlet.hubs.get_hub() + self.__dict__['_eventlet_listener'] = hub.add(hub.READ, + self.getsockopt(FD), + event, + lambda _: None, + lambda: None) + self.__dict__['_eventlet_clock'] = hub.clock + + @_wraps(_Socket.close) + def close(self, linger=None): + super().close(linger) + if self._eventlet_listener is not None: + eventlet.hubs.get_hub().remove(self._eventlet_listener) + self.__dict__['_eventlet_listener'] = None + # wake any blocked threads + self._eventlet_send_event.wake() + self._eventlet_recv_event.wake() + + @_wraps(_Socket.getsockopt) + def getsockopt(self, option): + result = _Socket_getsockopt(self, option) + if option == EVENTS: + # Getting the events causes the zmq socket to process + # events which may mean a msg can be sent or received. If + # there is a greenthread blocked and waiting for events, + # it will miss the edge-triggered read event, so wake it + # up. + if (result & POLLOUT): + self._eventlet_send_event.wake() + if (result & POLLIN): + self._eventlet_recv_event.wake() + return result + + @_wraps(_Socket.send) + def send(self, msg, flags=0, copy=True, track=False): + """A send method that's safe to use when multiple greenthreads + are calling send, send_multipart, recv and recv_multipart on + the same socket. + """ + if flags & NOBLOCK: + result = _Socket_send(self, msg, flags, copy, track) + # Instead of calling both wake methods, could call + # self.getsockopt(EVENTS) which would trigger wakeups if + # needed. + self._eventlet_send_event.wake() + self._eventlet_recv_event.wake() + return result + + # TODO: pyzmq will copy the message buffer and create Message + # objects under some circumstances. We could do that work here + # once to avoid doing it every time the send is retried. + flags |= NOBLOCK + with self._eventlet_send_lock: + while True: + try: + return _Socket_send(self, msg, flags, copy, track) + except ZMQError as e: + if e.errno == EAGAIN: + self._eventlet_send_event.block() + else: + raise + finally: + # The call to send processes 0mq events and may + # make the socket ready to recv. Wake the next + # receiver. (Could check EVENTS for POLLIN here) + self._eventlet_recv_event.wake() + + @_wraps(_Socket.send_multipart) + def send_multipart(self, msg_parts, flags=0, copy=True, track=False): + """A send_multipart method that's safe to use when multiple + greenthreads are calling send, send_multipart, recv and + recv_multipart on the same socket. + """ + if flags & NOBLOCK: + return _Socket_send_multipart(self, msg_parts, flags, copy, track) + + # acquire lock here so the subsequent calls to send for the + # message parts after the first don't block + with self._eventlet_send_lock: + return _Socket_send_multipart(self, msg_parts, flags, copy, track) + + @_wraps(_Socket.send_string) + def send_string(self, u, flags=0, copy=True, encoding='utf-8'): + """A send_string method that's safe to use when multiple + greenthreads are calling send, send_string, recv and + recv_string on the same socket. + """ + if flags & NOBLOCK: + return _Socket_send_string(self, u, flags, copy, encoding) + + # acquire lock here so the subsequent calls to send for the + # message parts after the first don't block + with self._eventlet_send_lock: + return _Socket_send_string(self, u, flags, copy, encoding) + + @_wraps(_Socket.send_pyobj) + def send_pyobj(self, obj, flags=0, protocol=2): + """A send_pyobj method that's safe to use when multiple + greenthreads are calling send, send_pyobj, recv and + recv_pyobj on the same socket. + """ + if flags & NOBLOCK: + return _Socket_send_pyobj(self, obj, flags, protocol) + + # acquire lock here so the subsequent calls to send for the + # message parts after the first don't block + with self._eventlet_send_lock: + return _Socket_send_pyobj(self, obj, flags, protocol) + + @_wraps(_Socket.send_json) + def send_json(self, obj, flags=0, **kwargs): + """A send_json method that's safe to use when multiple + greenthreads are calling send, send_json, recv and + recv_json on the same socket. + """ + if flags & NOBLOCK: + return _Socket_send_json(self, obj, flags, **kwargs) + + # acquire lock here so the subsequent calls to send for the + # message parts after the first don't block + with self._eventlet_send_lock: + return _Socket_send_json(self, obj, flags, **kwargs) + + @_wraps(_Socket.recv) + def recv(self, flags=0, copy=True, track=False): + """A recv method that's safe to use when multiple greenthreads + are calling send, send_multipart, recv and recv_multipart on + the same socket. + """ + if flags & NOBLOCK: + msg = _Socket_recv(self, flags, copy, track) + # Instead of calling both wake methods, could call + # self.getsockopt(EVENTS) which would trigger wakeups if + # needed. + self._eventlet_send_event.wake() + self._eventlet_recv_event.wake() + return msg + + deadline = None + if hasattr(__zmq__, 'RCVTIMEO'): + sock_timeout = self.getsockopt(__zmq__.RCVTIMEO) + if sock_timeout == -1: + pass + elif sock_timeout > 0: + deadline = self._eventlet_clock() + sock_timeout / 1000.0 + else: + raise ValueError(sock_timeout) + + flags |= NOBLOCK + with self._eventlet_recv_lock: + while True: + try: + return _Socket_recv(self, flags, copy, track) + except ZMQError as e: + if e.errno == EAGAIN: + # zmq in its wisdom decided to reuse EAGAIN for timeouts + if deadline is not None and self._eventlet_clock() > deadline: + e.is_timeout = True + raise + + self._eventlet_recv_event.block(deadline=deadline) + else: + raise + finally: + # The call to recv processes 0mq events and may + # make the socket ready to send. Wake the next + # receiver. (Could check EVENTS for POLLOUT here) + self._eventlet_send_event.wake() + + @_wraps(_Socket.recv_multipart) + def recv_multipart(self, flags=0, copy=True, track=False): + """A recv_multipart method that's safe to use when multiple + greenthreads are calling send, send_multipart, recv and + recv_multipart on the same socket. + """ + if flags & NOBLOCK: + return _Socket_recv_multipart(self, flags, copy, track) + + # acquire lock here so the subsequent calls to recv for the + # message parts after the first don't block + with self._eventlet_recv_lock: + return _Socket_recv_multipart(self, flags, copy, track) + + @_wraps(_Socket.recv_string) + def recv_string(self, flags=0, encoding='utf-8'): + """A recv_string method that's safe to use when multiple + greenthreads are calling send, send_string, recv and + recv_string on the same socket. + """ + if flags & NOBLOCK: + return _Socket_recv_string(self, flags, encoding) + + # acquire lock here so the subsequent calls to recv for the + # message parts after the first don't block + with self._eventlet_recv_lock: + return _Socket_recv_string(self, flags, encoding) + + @_wraps(_Socket.recv_json) + def recv_json(self, flags=0, **kwargs): + """A recv_json method that's safe to use when multiple + greenthreads are calling send, send_json, recv and + recv_json on the same socket. + """ + if flags & NOBLOCK: + return _Socket_recv_json(self, flags, **kwargs) + + # acquire lock here so the subsequent calls to recv for the + # message parts after the first don't block + with self._eventlet_recv_lock: + return _Socket_recv_json(self, flags, **kwargs) + + @_wraps(_Socket.recv_pyobj) + def recv_pyobj(self, flags=0): + """A recv_pyobj method that's safe to use when multiple + greenthreads are calling send, send_pyobj, recv and + recv_pyobj on the same socket. + """ + if flags & NOBLOCK: + return _Socket_recv_pyobj(self, flags) + + # acquire lock here so the subsequent calls to recv for the + # message parts after the first don't block + with self._eventlet_recv_lock: + return _Socket_recv_pyobj(self, flags) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/greenio/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/greenio/__init__.py new file mode 100644 index 0000000..513c4a5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/greenio/__init__.py @@ -0,0 +1,3 @@ +from eventlet.greenio.base import * # noqa + +from eventlet.greenio.py3 import * # noqa diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/greenio/base.py b/netdeploy/lib/python3.11/site-packages/eventlet/greenio/base.py new file mode 100644 index 0000000..3bb7d02 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/greenio/base.py @@ -0,0 +1,485 @@ +import errno +import os +import socket +import sys +import time +import warnings + +import eventlet +from eventlet.hubs import trampoline, notify_opened, IOClosed +from eventlet.support import get_errno + +__all__ = [ + 'GreenSocket', '_GLOBAL_DEFAULT_TIMEOUT', 'set_nonblocking', + 'SOCKET_BLOCKING', 'SOCKET_CLOSED', 'CONNECT_ERR', 'CONNECT_SUCCESS', + 'shutdown_safe', 'SSL', + 'socket_timeout', +] + +BUFFER_SIZE = 4096 +CONNECT_ERR = {errno.EINPROGRESS, errno.EALREADY, errno.EWOULDBLOCK} +CONNECT_SUCCESS = {0, errno.EISCONN} +if sys.platform[:3] == "win": + CONNECT_ERR.add(errno.WSAEINVAL) # Bug 67 + +_original_socket = eventlet.patcher.original('socket').socket + + +if sys.version_info >= (3, 10): + socket_timeout = socket.timeout # Really, TimeoutError +else: + socket_timeout = eventlet.timeout.wrap_is_timeout(socket.timeout) + + +def socket_connect(descriptor, address): + """ + Attempts to connect to the address, returns the descriptor if it succeeds, + returns None if it needs to trampoline, and raises any exceptions. + """ + err = descriptor.connect_ex(address) + if err in CONNECT_ERR: + return None + if err not in CONNECT_SUCCESS: + raise OSError(err, errno.errorcode[err]) + return descriptor + + +def socket_checkerr(descriptor): + err = descriptor.getsockopt(socket.SOL_SOCKET, socket.SO_ERROR) + if err not in CONNECT_SUCCESS: + raise OSError(err, errno.errorcode[err]) + + +def socket_accept(descriptor): + """ + Attempts to accept() on the descriptor, returns a client,address tuple + if it succeeds; returns None if it needs to trampoline, and raises + any exceptions. + """ + try: + return descriptor.accept() + except OSError as e: + if get_errno(e) == errno.EWOULDBLOCK: + return None + raise + + +if sys.platform[:3] == "win": + # winsock sometimes throws ENOTCONN + SOCKET_BLOCKING = {errno.EAGAIN, errno.EWOULDBLOCK} + SOCKET_CLOSED = {errno.ECONNRESET, errno.ENOTCONN, errno.ESHUTDOWN} +else: + # oddly, on linux/darwin, an unconnected socket is expected to block, + # so we treat ENOTCONN the same as EWOULDBLOCK + SOCKET_BLOCKING = {errno.EAGAIN, errno.EWOULDBLOCK, errno.ENOTCONN} + SOCKET_CLOSED = {errno.ECONNRESET, errno.ESHUTDOWN, errno.EPIPE} + + +def set_nonblocking(fd): + """ + Sets the descriptor to be nonblocking. Works on many file-like + objects as well as sockets. Only sockets can be nonblocking on + Windows, however. + """ + try: + setblocking = fd.setblocking + except AttributeError: + # fd has no setblocking() method. It could be that this version of + # Python predates socket.setblocking(). In that case, we can still set + # the flag "by hand" on the underlying OS fileno using the fcntl + # module. + try: + import fcntl + except ImportError: + # Whoops, Windows has no fcntl module. This might not be a socket + # at all, but rather a file-like object with no setblocking() + # method. In particular, on Windows, pipes don't support + # non-blocking I/O and therefore don't have that method. Which + # means fcntl wouldn't help even if we could load it. + raise NotImplementedError("set_nonblocking() on a file object " + "with no setblocking() method " + "(Windows pipes don't support non-blocking I/O)") + # We managed to import fcntl. + fileno = fd.fileno() + orig_flags = fcntl.fcntl(fileno, fcntl.F_GETFL) + new_flags = orig_flags | os.O_NONBLOCK + if new_flags != orig_flags: + fcntl.fcntl(fileno, fcntl.F_SETFL, new_flags) + else: + # socket supports setblocking() + setblocking(0) + + +try: + from socket import _GLOBAL_DEFAULT_TIMEOUT +except ImportError: + _GLOBAL_DEFAULT_TIMEOUT = object() + + +class GreenSocket: + """ + Green version of socket.socket class, that is intended to be 100% + API-compatible. + + It also recognizes the keyword parameter, 'set_nonblocking=True'. + Pass False to indicate that socket is already in non-blocking mode + to save syscalls. + """ + + # This placeholder is to prevent __getattr__ from creating an infinite call loop + fd = None + + def __init__(self, family=socket.AF_INET, *args, **kwargs): + should_set_nonblocking = kwargs.pop('set_nonblocking', True) + if isinstance(family, int): + fd = _original_socket(family, *args, **kwargs) + # Notify the hub that this is a newly-opened socket. + notify_opened(fd.fileno()) + else: + fd = family + + # import timeout from other socket, if it was there + try: + self._timeout = fd.gettimeout() or socket.getdefaulttimeout() + except AttributeError: + self._timeout = socket.getdefaulttimeout() + + # Filter fd.fileno() != -1 so that won't call set non-blocking on + # closed socket + if should_set_nonblocking and fd.fileno() != -1: + set_nonblocking(fd) + self.fd = fd + # when client calls setblocking(0) or settimeout(0) the socket must + # act non-blocking + self.act_non_blocking = False + + # Copy some attributes from underlying real socket. + # This is the easiest way that i found to fix + # https://bitbucket.org/eventlet/eventlet/issue/136 + # Only `getsockopt` is required to fix that issue, others + # are just premature optimization to save __getattr__ call. + self.bind = fd.bind + self.close = fd.close + self.fileno = fd.fileno + self.getsockname = fd.getsockname + self.getsockopt = fd.getsockopt + self.listen = fd.listen + self.setsockopt = fd.setsockopt + self.shutdown = fd.shutdown + self._closed = False + + @property + def _sock(self): + return self + + def _get_io_refs(self): + return self.fd._io_refs + + def _set_io_refs(self, value): + self.fd._io_refs = value + + _io_refs = property(_get_io_refs, _set_io_refs) + + # Forward unknown attributes to fd, cache the value for future use. + # I do not see any simple attribute which could be changed + # so caching everything in self is fine. + # If we find such attributes - only attributes having __get__ might be cached. + # For now - I do not want to complicate it. + def __getattr__(self, name): + if self.fd is None: + raise AttributeError(name) + attr = getattr(self.fd, name) + setattr(self, name, attr) + return attr + + def _trampoline(self, fd, read=False, write=False, timeout=None, timeout_exc=None): + """ We need to trampoline via the event hub. + We catch any signal back from the hub indicating that the operation we + were waiting on was associated with a filehandle that's since been + invalidated. + """ + if self._closed: + # If we did any logging, alerting to a second trampoline attempt on a closed + # socket here would be useful. + raise IOClosed() + try: + return trampoline(fd, read=read, write=write, timeout=timeout, + timeout_exc=timeout_exc, + mark_as_closed=self._mark_as_closed) + except IOClosed: + # This socket's been obsoleted. De-fang it. + self._mark_as_closed() + raise + + def accept(self): + if self.act_non_blocking: + res = self.fd.accept() + notify_opened(res[0].fileno()) + return res + fd = self.fd + _timeout_exc = socket_timeout('timed out') + while True: + res = socket_accept(fd) + if res is not None: + client, addr = res + notify_opened(client.fileno()) + set_nonblocking(client) + return type(self)(client), addr + self._trampoline(fd, read=True, timeout=self.gettimeout(), timeout_exc=_timeout_exc) + + def _mark_as_closed(self): + """ Mark this socket as being closed """ + self._closed = True + + def __del__(self): + # This is in case self.close is not assigned yet (currently the constructor does it) + close = getattr(self, 'close', None) + if close is not None: + close() + + def connect(self, address): + if self.act_non_blocking: + return self.fd.connect(address) + fd = self.fd + _timeout_exc = socket_timeout('timed out') + if self.gettimeout() is None: + while not socket_connect(fd, address): + try: + self._trampoline(fd, write=True) + except IOClosed: + raise OSError(errno.EBADFD) + socket_checkerr(fd) + else: + end = time.time() + self.gettimeout() + while True: + if socket_connect(fd, address): + return + if time.time() >= end: + raise _timeout_exc + timeout = end - time.time() + try: + self._trampoline(fd, write=True, timeout=timeout, timeout_exc=_timeout_exc) + except IOClosed: + # ... we need some workable errno here. + raise OSError(errno.EBADFD) + socket_checkerr(fd) + + def connect_ex(self, address): + if self.act_non_blocking: + return self.fd.connect_ex(address) + fd = self.fd + if self.gettimeout() is None: + while not socket_connect(fd, address): + try: + self._trampoline(fd, write=True) + socket_checkerr(fd) + except OSError as ex: + return get_errno(ex) + except IOClosed: + return errno.EBADFD + return 0 + else: + end = time.time() + self.gettimeout() + timeout_exc = socket.timeout(errno.EAGAIN) + while True: + try: + if socket_connect(fd, address): + return 0 + if time.time() >= end: + raise timeout_exc + self._trampoline(fd, write=True, timeout=end - time.time(), + timeout_exc=timeout_exc) + socket_checkerr(fd) + except OSError as ex: + return get_errno(ex) + except IOClosed: + return errno.EBADFD + return 0 + + def dup(self, *args, **kw): + sock = self.fd.dup(*args, **kw) + newsock = type(self)(sock, set_nonblocking=False) + newsock.settimeout(self.gettimeout()) + return newsock + + def makefile(self, *args, **kwargs): + return _original_socket.makefile(self, *args, **kwargs) + + def makeGreenFile(self, *args, **kw): + warnings.warn("makeGreenFile has been deprecated, please use " + "makefile instead", DeprecationWarning, stacklevel=2) + return self.makefile(*args, **kw) + + def _read_trampoline(self): + self._trampoline( + self.fd, + read=True, + timeout=self.gettimeout(), + timeout_exc=socket_timeout('timed out')) + + def _recv_loop(self, recv_meth, empty_val, *args): + if self.act_non_blocking: + return recv_meth(*args) + + while True: + try: + # recv: bufsize=0? + # recv_into: buffer is empty? + # This is needed because behind the scenes we use sockets in + # nonblocking mode and builtin recv* methods. Attempting to read + # 0 bytes from a nonblocking socket using a builtin recv* method + # does not raise a timeout exception. Since we're simulating + # a blocking socket here we need to produce a timeout exception + # if needed, hence the call to trampoline. + if not args[0]: + self._read_trampoline() + return recv_meth(*args) + except OSError as e: + if get_errno(e) in SOCKET_BLOCKING: + pass + elif get_errno(e) in SOCKET_CLOSED: + return empty_val + else: + raise + + try: + self._read_trampoline() + except IOClosed as e: + # Perhaps we should return '' instead? + raise EOFError() + + def recv(self, bufsize, flags=0): + return self._recv_loop(self.fd.recv, b'', bufsize, flags) + + def recvfrom(self, bufsize, flags=0): + return self._recv_loop(self.fd.recvfrom, b'', bufsize, flags) + + def recv_into(self, buffer, nbytes=0, flags=0): + return self._recv_loop(self.fd.recv_into, 0, buffer, nbytes, flags) + + def recvfrom_into(self, buffer, nbytes=0, flags=0): + return self._recv_loop(self.fd.recvfrom_into, 0, buffer, nbytes, flags) + + def _send_loop(self, send_method, data, *args): + if self.act_non_blocking: + return send_method(data, *args) + + _timeout_exc = socket_timeout('timed out') + while True: + try: + return send_method(data, *args) + except OSError as e: + eno = get_errno(e) + if eno == errno.ENOTCONN or eno not in SOCKET_BLOCKING: + raise + + try: + self._trampoline(self.fd, write=True, timeout=self.gettimeout(), + timeout_exc=_timeout_exc) + except IOClosed: + raise OSError(errno.ECONNRESET, 'Connection closed by another thread') + + def send(self, data, flags=0): + return self._send_loop(self.fd.send, data, flags) + + def sendto(self, data, *args): + return self._send_loop(self.fd.sendto, data, *args) + + def sendall(self, data, flags=0): + tail = self.send(data, flags) + len_data = len(data) + while tail < len_data: + tail += self.send(data[tail:], flags) + + def setblocking(self, flag): + if flag: + self.act_non_blocking = False + self._timeout = None + else: + self.act_non_blocking = True + self._timeout = 0.0 + + def settimeout(self, howlong): + if howlong is None or howlong == _GLOBAL_DEFAULT_TIMEOUT: + self.setblocking(True) + return + try: + f = howlong.__float__ + except AttributeError: + raise TypeError('a float is required') + howlong = f() + if howlong < 0.0: + raise ValueError('Timeout value out of range') + if howlong == 0.0: + self.act_non_blocking = True + self._timeout = 0.0 + else: + self.act_non_blocking = False + self._timeout = howlong + + def gettimeout(self): + return self._timeout + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + +def _operation_on_closed_file(*args, **kwargs): + raise ValueError("I/O operation on closed file") + + +greenpipe_doc = """ + GreenPipe is a cooperative replacement for file class. + It will cooperate on pipes. It will block on regular file. + Differences from file class: + - mode is r/w property. Should re r/o + - encoding property not implemented + - write/writelines will not raise TypeError exception when non-string data is written + it will write str(data) instead + - Universal new lines are not supported and newlines property not implementeded + - file argument can be descriptor, file name or file object. + """ + +# import SSL module here so we can refer to greenio.SSL.exceptionclass +try: + from OpenSSL import SSL +except ImportError: + # pyOpenSSL not installed, define exceptions anyway for convenience + class SSL: + class WantWriteError(Exception): + pass + + class WantReadError(Exception): + pass + + class ZeroReturnError(Exception): + pass + + class SysCallError(Exception): + pass + + +def shutdown_safe(sock): + """Shuts down the socket. This is a convenience method for + code that wants to gracefully handle regular sockets, SSL.Connection + sockets from PyOpenSSL and ssl.SSLSocket objects from Python 2.7 interchangeably. + Both types of ssl socket require a shutdown() before close, + but they have different arity on their shutdown method. + + Regular sockets don't need a shutdown before close, but it doesn't hurt. + """ + try: + try: + # socket, ssl.SSLSocket + return sock.shutdown(socket.SHUT_RDWR) + except TypeError: + # SSL.Connection + return sock.shutdown() + except OSError as e: + # we don't care if the socket is already closed; + # this will often be the case in an http server context + if get_errno(e) not in (errno.ENOTCONN, errno.EBADF, errno.ENOTSOCK): + raise diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/greenio/py3.py b/netdeploy/lib/python3.11/site-packages/eventlet/greenio/py3.py new file mode 100644 index 0000000..d3811df --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/greenio/py3.py @@ -0,0 +1,227 @@ +import _pyio as _original_pyio +import errno +import os as _original_os +import socket as _original_socket +from io import ( + BufferedRandom as _OriginalBufferedRandom, + BufferedReader as _OriginalBufferedReader, + BufferedWriter as _OriginalBufferedWriter, + DEFAULT_BUFFER_SIZE, + TextIOWrapper as _OriginalTextIOWrapper, + IOBase as _OriginalIOBase, +) +from types import FunctionType + +from eventlet.greenio.base import ( + _operation_on_closed_file, + greenpipe_doc, + set_nonblocking, + SOCKET_BLOCKING, +) +from eventlet.hubs import notify_close, notify_opened, IOClosed, trampoline +from eventlet.support import get_errno + +__all__ = ['_fileobject', 'GreenPipe'] + +# TODO get rid of this, it only seems like the original _fileobject +_fileobject = _original_socket.SocketIO + +# Large part of the following code is copied from the original +# eventlet.greenio module + + +class GreenFileIO(_OriginalIOBase): + + _blksize = 128 * 1024 + + def __init__(self, name, mode='r', closefd=True, opener=None): + if isinstance(name, int): + fileno = name + self._name = "" % fileno + else: + assert isinstance(name, str) + with open(name, mode) as fd: + self._name = fd.name + fileno = _original_os.dup(fd.fileno()) + + notify_opened(fileno) + self._fileno = fileno + self._mode = mode + self._closed = False + set_nonblocking(self) + self._seekable = None + + @property + def closed(self): + return self._closed + + def seekable(self): + if self._seekable is None: + try: + _original_os.lseek(self._fileno, 0, _original_os.SEEK_CUR) + except OSError as e: + if get_errno(e) == errno.ESPIPE: + self._seekable = False + else: + raise + else: + self._seekable = True + + return self._seekable + + def readable(self): + return 'r' in self._mode or '+' in self._mode + + def writable(self): + return 'w' in self._mode or '+' in self._mode or 'a' in self._mode + + def fileno(self): + return self._fileno + + def read(self, size=-1): + if size == -1: + return self.readall() + + while True: + try: + return _original_os.read(self._fileno, size) + except OSError as e: + if get_errno(e) not in SOCKET_BLOCKING: + raise OSError(*e.args) + self._trampoline(self, read=True) + + def readall(self): + buf = [] + while True: + try: + chunk = _original_os.read(self._fileno, DEFAULT_BUFFER_SIZE) + if chunk == b'': + return b''.join(buf) + buf.append(chunk) + except OSError as e: + if get_errno(e) not in SOCKET_BLOCKING: + raise OSError(*e.args) + self._trampoline(self, read=True) + + def readinto(self, b): + up_to = len(b) + data = self.read(up_to) + bytes_read = len(data) + b[:bytes_read] = data + return bytes_read + + def isatty(self): + try: + return _original_os.isatty(self.fileno()) + except OSError as e: + raise OSError(*e.args) + + def _isatty_open_only(self): + # Python does an optimization here, not going to bother and just do the + # slow path. + return self.isatty() + + def _trampoline(self, fd, read=False, write=False, timeout=None, timeout_exc=None): + if self._closed: + # Don't trampoline if we're already closed. + raise IOClosed() + try: + return trampoline(fd, read=read, write=write, timeout=timeout, + timeout_exc=timeout_exc, + mark_as_closed=self._mark_as_closed) + except IOClosed: + # Our fileno has been obsoleted. Defang ourselves to + # prevent spurious closes. + self._mark_as_closed() + raise + + def _mark_as_closed(self): + """ Mark this socket as being closed """ + self._closed = True + + def write(self, data): + view = memoryview(data) + datalen = len(data) + offset = 0 + while offset < datalen: + try: + written = _original_os.write(self._fileno, view[offset:]) + except OSError as e: + if get_errno(e) not in SOCKET_BLOCKING: + raise OSError(*e.args) + trampoline(self, write=True) + else: + offset += written + return offset + + def close(self): + if not self._closed: + self._closed = True + _original_os.close(self._fileno) + notify_close(self._fileno) + for method in [ + 'fileno', 'flush', 'isatty', 'next', 'read', 'readinto', + 'readline', 'readlines', 'seek', 'tell', 'truncate', + 'write', 'xreadlines', '__iter__', '__next__', 'writelines']: + setattr(self, method, _operation_on_closed_file) + + def truncate(self, size=-1): + if size is None: + size = -1 + if size == -1: + size = self.tell() + try: + rv = _original_os.ftruncate(self._fileno, size) + except OSError as e: + raise OSError(*e.args) + else: + self.seek(size) # move position&clear buffer + return rv + + def seek(self, offset, whence=_original_os.SEEK_SET): + try: + return _original_os.lseek(self._fileno, offset, whence) + except OSError as e: + raise OSError(*e.args) + + def __enter__(self): + return self + + def __exit__(self, *args): + self.close() + + +_open_environment = dict(globals()) +_open_environment.update(dict( + BufferedRandom=_OriginalBufferedRandom, + BufferedWriter=_OriginalBufferedWriter, + BufferedReader=_OriginalBufferedReader, + TextIOWrapper=_OriginalTextIOWrapper, + FileIO=GreenFileIO, + os=_original_os, +)) +if hasattr(_original_pyio, 'text_encoding'): + _open_environment['text_encoding'] = _original_pyio.text_encoding + +_pyio_open = getattr(_original_pyio.open, '__wrapped__', _original_pyio.open) +_open = FunctionType( + _pyio_open.__code__, + _open_environment, +) + + +def GreenPipe(name, mode="r", buffering=-1, encoding=None, errors=None, + newline=None, closefd=True, opener=None): + try: + fileno = name.fileno() + except AttributeError: + pass + else: + fileno = _original_os.dup(fileno) + name.close() + name = fileno + + return _open(name, mode, buffering, encoding, errors, newline, closefd, opener) + + +GreenPipe.__doc__ = greenpipe_doc diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/greenpool.py b/netdeploy/lib/python3.11/site-packages/eventlet/greenpool.py new file mode 100644 index 0000000..f907e38 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/greenpool.py @@ -0,0 +1,254 @@ +import traceback + +import eventlet +from eventlet import queue +from eventlet.support import greenlets as greenlet + +__all__ = ['GreenPool', 'GreenPile'] + +DEBUG = True + + +class GreenPool: + """The GreenPool class is a pool of green threads. + """ + + def __init__(self, size=1000): + try: + size = int(size) + except ValueError as e: + msg = 'GreenPool() expect size :: int, actual: {} {}'.format(type(size), str(e)) + raise TypeError(msg) + if size < 0: + msg = 'GreenPool() expect size >= 0, actual: {}'.format(repr(size)) + raise ValueError(msg) + self.size = size + self.coroutines_running = set() + self.sem = eventlet.Semaphore(size) + self.no_coros_running = eventlet.Event() + + def resize(self, new_size): + """ Change the max number of greenthreads doing work at any given time. + + If resize is called when there are more than *new_size* greenthreads + already working on tasks, they will be allowed to complete but no new + tasks will be allowed to get launched until enough greenthreads finish + their tasks to drop the overall quantity below *new_size*. Until + then, the return value of free() will be negative. + """ + size_delta = new_size - self.size + self.sem.counter += size_delta + self.size = new_size + + def running(self): + """ Returns the number of greenthreads that are currently executing + functions in the GreenPool.""" + return len(self.coroutines_running) + + def free(self): + """ Returns the number of greenthreads available for use. + + If zero or less, the next call to :meth:`spawn` or :meth:`spawn_n` will + block the calling greenthread until a slot becomes available.""" + return self.sem.counter + + def spawn(self, function, *args, **kwargs): + """Run the *function* with its arguments in its own green thread. + Returns the :class:`GreenThread ` + object that is running the function, which can be used to retrieve the + results. + + If the pool is currently at capacity, ``spawn`` will block until one of + the running greenthreads completes its task and frees up a slot. + + This function is reentrant; *function* can call ``spawn`` on the same + pool without risk of deadlocking the whole thing. + """ + # if reentering an empty pool, don't try to wait on a coroutine freeing + # itself -- instead, just execute in the current coroutine + current = eventlet.getcurrent() + if self.sem.locked() and current in self.coroutines_running: + # a bit hacky to use the GT without switching to it + gt = eventlet.greenthread.GreenThread(current) + gt.main(function, args, kwargs) + return gt + else: + self.sem.acquire() + gt = eventlet.spawn(function, *args, **kwargs) + if not self.coroutines_running: + self.no_coros_running = eventlet.Event() + self.coroutines_running.add(gt) + gt.link(self._spawn_done) + return gt + + def _spawn_n_impl(self, func, args, kwargs, coro): + try: + try: + func(*args, **kwargs) + except (KeyboardInterrupt, SystemExit, greenlet.GreenletExit): + raise + except: + if DEBUG: + traceback.print_exc() + finally: + if coro is not None: + coro = eventlet.getcurrent() + self._spawn_done(coro) + + def spawn_n(self, function, *args, **kwargs): + """Create a greenthread to run the *function*, the same as + :meth:`spawn`. The difference is that :meth:`spawn_n` returns + None; the results of *function* are not retrievable. + """ + # if reentering an empty pool, don't try to wait on a coroutine freeing + # itself -- instead, just execute in the current coroutine + current = eventlet.getcurrent() + if self.sem.locked() and current in self.coroutines_running: + self._spawn_n_impl(function, args, kwargs, None) + else: + self.sem.acquire() + g = eventlet.spawn_n( + self._spawn_n_impl, + function, args, kwargs, True) + if not self.coroutines_running: + self.no_coros_running = eventlet.Event() + self.coroutines_running.add(g) + + def waitall(self): + """Waits until all greenthreads in the pool are finished working.""" + assert eventlet.getcurrent() not in self.coroutines_running, \ + "Calling waitall() from within one of the " \ + "GreenPool's greenthreads will never terminate." + if self.running(): + self.no_coros_running.wait() + + def _spawn_done(self, coro): + self.sem.release() + if coro is not None: + self.coroutines_running.remove(coro) + # if done processing (no more work is waiting for processing), + # we can finish off any waitall() calls that might be pending + if self.sem.balance == self.size: + self.no_coros_running.send(None) + + def waiting(self): + """Return the number of greenthreads waiting to spawn. + """ + if self.sem.balance < 0: + return -self.sem.balance + else: + return 0 + + def _do_map(self, func, it, gi): + for args in it: + gi.spawn(func, *args) + gi.done_spawning() + + def starmap(self, function, iterable): + """This is the same as :func:`itertools.starmap`, except that *func* is + executed in a separate green thread for each item, with the concurrency + limited by the pool's size. In operation, starmap consumes a constant + amount of memory, proportional to the size of the pool, and is thus + suited for iterating over extremely long input lists. + """ + if function is None: + function = lambda *a: a + # We use a whole separate greenthread so its spawn() calls can block + # without blocking OUR caller. On the other hand, we must assume that + # our caller will immediately start trying to iterate over whatever we + # return. If that were a GreenPile, our caller would always see an + # empty sequence because the hub hasn't even entered _do_map() yet -- + # _do_map() hasn't had a chance to spawn a single greenthread on this + # GreenPool! A GreenMap is safe to use with different producer and + # consumer greenthreads, because it doesn't raise StopIteration until + # the producer has explicitly called done_spawning(). + gi = GreenMap(self.size) + eventlet.spawn_n(self._do_map, function, iterable, gi) + return gi + + def imap(self, function, *iterables): + """This is the same as :func:`itertools.imap`, and has the same + concurrency and memory behavior as :meth:`starmap`. + + It's quite convenient for, e.g., farming out jobs from a file:: + + def worker(line): + return do_something(line) + pool = GreenPool() + for result in pool.imap(worker, open("filename", 'r')): + print(result) + """ + return self.starmap(function, zip(*iterables)) + + +class GreenPile: + """GreenPile is an abstraction representing a bunch of I/O-related tasks. + + Construct a GreenPile with an existing GreenPool object. The GreenPile will + then use that pool's concurrency as it processes its jobs. There can be + many GreenPiles associated with a single GreenPool. + + A GreenPile can also be constructed standalone, not associated with any + GreenPool. To do this, construct it with an integer size parameter instead + of a GreenPool. + + It is not advisable to iterate over a GreenPile in a different greenthread + than the one which is calling spawn. The iterator will exit early in that + situation. + """ + + def __init__(self, size_or_pool=1000): + if isinstance(size_or_pool, GreenPool): + self.pool = size_or_pool + else: + self.pool = GreenPool(size_or_pool) + self.waiters = queue.LightQueue() + self.counter = 0 + + def spawn(self, func, *args, **kw): + """Runs *func* in its own green thread, with the result available by + iterating over the GreenPile object.""" + self.counter += 1 + try: + gt = self.pool.spawn(func, *args, **kw) + self.waiters.put(gt) + except: + self.counter -= 1 + raise + + def __iter__(self): + return self + + def next(self): + """Wait for the next result, suspending the current greenthread until it + is available. Raises StopIteration when there are no more results.""" + if self.counter == 0: + raise StopIteration() + return self._next() + __next__ = next + + def _next(self): + try: + return self.waiters.get().wait() + finally: + self.counter -= 1 + + +# this is identical to GreenPile but it blocks on spawn if the results +# aren't consumed, and it doesn't generate its own StopIteration exception, +# instead relying on the spawning process to send one in when it's done +class GreenMap(GreenPile): + def __init__(self, size_or_pool): + super().__init__(size_or_pool) + self.waiters = queue.LightQueue(maxsize=self.pool.size) + + def done_spawning(self): + self.spawn(lambda: StopIteration()) + + def next(self): + val = self._next() + if isinstance(val, StopIteration): + raise val + else: + return val + __next__ = next diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/greenthread.py b/netdeploy/lib/python3.11/site-packages/eventlet/greenthread.py new file mode 100644 index 0000000..d1be005 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/greenthread.py @@ -0,0 +1,353 @@ +from collections import deque +import sys + +from greenlet import GreenletExit + +from eventlet import event +from eventlet import hubs +from eventlet import support +from eventlet import timeout +from eventlet.hubs import timer +from eventlet.support import greenlets as greenlet +import warnings + +__all__ = ['getcurrent', 'sleep', 'spawn', 'spawn_n', + 'kill', + 'spawn_after', 'spawn_after_local', 'GreenThread'] + +getcurrent = greenlet.getcurrent + + +def sleep(seconds=0): + """Yield control to another eligible coroutine until at least *seconds* have + elapsed. + + *seconds* may be specified as an integer, or a float if fractional seconds + are desired. Calling :func:`~greenthread.sleep` with *seconds* of 0 is the + canonical way of expressing a cooperative yield. For example, if one is + looping over a large list performing an expensive calculation without + calling any socket methods, it's a good idea to call ``sleep(0)`` + occasionally; otherwise nothing else will run. + """ + hub = hubs.get_hub() + current = getcurrent() + if hub.greenlet is current: + if seconds <= 0: + # In this case, sleep(0) got called in the event loop threadlet. + # This isn't blocking, so it's not harmful. And it will not be + # possible to switch in this situation. So not much we can do other + # than just keep running. This does get triggered in real code, + # unfortunately. + return + raise RuntimeError('do not call blocking functions from the mainloop') + timer = hub.schedule_call_global(seconds, current.switch) + try: + hub.switch() + finally: + timer.cancel() + + +def spawn(func, *args, **kwargs): + """Create a greenthread to run ``func(*args, **kwargs)``. Returns a + :class:`GreenThread` object which you can use to get the results of the + call. + + Execution control returns immediately to the caller; the created greenthread + is merely scheduled to be run at the next available opportunity. + Use :func:`spawn_after` to arrange for greenthreads to be spawned + after a finite delay. + """ + hub = hubs.get_hub() + g = GreenThread(hub.greenlet) + hub.schedule_call_global(0, g.switch, func, args, kwargs) + return g + + +def spawn_n(func, *args, **kwargs): + """Same as :func:`spawn`, but returns a ``greenlet`` object from + which it is not possible to retrieve either a return value or + whether it raised any exceptions. This is faster than + :func:`spawn`; it is fastest if there are no keyword arguments. + + If an exception is raised in the function, spawn_n prints a stack + trace; the print can be disabled by calling + :func:`eventlet.debug.hub_exceptions` with False. + """ + return _spawn_n(0, func, args, kwargs)[1] + + +def spawn_after(seconds, func, *args, **kwargs): + """Spawns *func* after *seconds* have elapsed. It runs as scheduled even if + the current greenthread has completed. + + *seconds* may be specified as an integer, or a float if fractional seconds + are desired. The *func* will be called with the given *args* and + keyword arguments *kwargs*, and will be executed within its own greenthread. + + The return value of :func:`spawn_after` is a :class:`GreenThread` object, + which can be used to retrieve the results of the call. + + To cancel the spawn and prevent *func* from being called, + call :meth:`GreenThread.cancel` on the return value of :func:`spawn_after`. + This will not abort the function if it's already started running, which is + generally the desired behavior. If terminating *func* regardless of whether + it's started or not is the desired behavior, call :meth:`GreenThread.kill`. + """ + hub = hubs.get_hub() + g = GreenThread(hub.greenlet) + hub.schedule_call_global(seconds, g.switch, func, args, kwargs) + return g + + +def spawn_after_local(seconds, func, *args, **kwargs): + """Spawns *func* after *seconds* have elapsed. The function will NOT be + called if the current greenthread has exited. + + *seconds* may be specified as an integer, or a float if fractional seconds + are desired. The *func* will be called with the given *args* and + keyword arguments *kwargs*, and will be executed within its own greenthread. + + The return value of :func:`spawn_after` is a :class:`GreenThread` object, + which can be used to retrieve the results of the call. + + To cancel the spawn and prevent *func* from being called, + call :meth:`GreenThread.cancel` on the return value. This will not abort the + function if it's already started running. If terminating *func* regardless + of whether it's started or not is the desired behavior, call + :meth:`GreenThread.kill`. + """ + hub = hubs.get_hub() + g = GreenThread(hub.greenlet) + hub.schedule_call_local(seconds, g.switch, func, args, kwargs) + return g + + +def call_after_global(seconds, func, *args, **kwargs): + warnings.warn( + "call_after_global is renamed to spawn_after, which" + "has the same signature and semantics (plus a bit extra). Please do a" + " quick search-and-replace on your codebase, thanks!", + DeprecationWarning, stacklevel=2) + return _spawn_n(seconds, func, args, kwargs)[0] + + +def call_after_local(seconds, function, *args, **kwargs): + warnings.warn( + "call_after_local is renamed to spawn_after_local, which" + "has the same signature and semantics (plus a bit extra).", + DeprecationWarning, stacklevel=2) + hub = hubs.get_hub() + g = greenlet.greenlet(function, parent=hub.greenlet) + t = hub.schedule_call_local(seconds, g.switch, *args, **kwargs) + return t + + +call_after = call_after_local + + +def exc_after(seconds, *throw_args): + warnings.warn("Instead of exc_after, which is deprecated, use " + "Timeout(seconds, exception)", + DeprecationWarning, stacklevel=2) + if seconds is None: # dummy argument, do nothing + return timer.Timer(seconds, lambda: None) + hub = hubs.get_hub() + return hub.schedule_call_local(seconds, getcurrent().throw, *throw_args) + + +# deprecate, remove +TimeoutError, with_timeout = ( + support.wrap_deprecated(old, new)(fun) for old, new, fun in ( + ('greenthread.TimeoutError', 'Timeout', timeout.Timeout), + ('greenthread.with_timeout', 'with_timeout', timeout.with_timeout), + )) + + +def _spawn_n(seconds, func, args, kwargs): + hub = hubs.get_hub() + g = greenlet.greenlet(func, parent=hub.greenlet) + t = hub.schedule_call_global(seconds, g.switch, *args, **kwargs) + return t, g + + +class GreenThread(greenlet.greenlet): + """The GreenThread class is a type of Greenlet which has the additional + property of being able to retrieve the return value of the main function. + Do not construct GreenThread objects directly; call :func:`spawn` to get one. + """ + + def __init__(self, parent): + greenlet.greenlet.__init__(self, self.main, parent) + self._exit_event = event.Event() + self._resolving_links = False + self._exit_funcs = None + + def __await__(self): + """ + Enable ``GreenThread``s to be ``await``ed in ``async`` functions. + """ + from eventlet.hubs.asyncio import Hub + hub = hubs.get_hub() + if not isinstance(hub, Hub): + raise RuntimeError( + "This API only works with eventlet's asyncio hub. " + + "To use it, set an EVENTLET_HUB=asyncio environment variable." + ) + + future = hub.loop.create_future() + + # When the Future finishes, check if it was due to cancellation: + def got_future_result(future): + if future.cancelled() and not self.dead: + # GreenThread is still running, so kill it: + self.kill() + + future.add_done_callback(got_future_result) + + # When the GreenThread finishes, set its result on the Future: + def got_gthread_result(gthread): + if future.done(): + # Can't set values any more. + return + + try: + # Should return immediately: + result = gthread.wait() + future.set_result(result) + except GreenletExit: + future.cancel() + except BaseException as e: + future.set_exception(e) + + self.link(got_gthread_result) + + return future.__await__() + + def wait(self): + """ Returns the result of the main function of this GreenThread. If the + result is a normal return value, :meth:`wait` returns it. If it raised + an exception, :meth:`wait` will raise the same exception (though the + stack trace will unavoidably contain some frames from within the + greenthread module).""" + return self._exit_event.wait() + + def link(self, func, *curried_args, **curried_kwargs): + """ Set up a function to be called with the results of the GreenThread. + + The function must have the following signature:: + + def func(gt, [curried args/kwargs]): + + When the GreenThread finishes its run, it calls *func* with itself + and with the `curried arguments `_ supplied + at link-time. If the function wants to retrieve the result of the GreenThread, + it should call wait() on its first argument. + + Note that *func* is called within execution context of + the GreenThread, so it is possible to interfere with other linked + functions by doing things like switching explicitly to another + greenthread. + """ + if self._exit_funcs is None: + self._exit_funcs = deque() + self._exit_funcs.append((func, curried_args, curried_kwargs)) + if self._exit_event.ready(): + self._resolve_links() + + def unlink(self, func, *curried_args, **curried_kwargs): + """ remove linked function set by :meth:`link` + + Remove successfully return True, otherwise False + """ + if not self._exit_funcs: + return False + try: + self._exit_funcs.remove((func, curried_args, curried_kwargs)) + return True + except ValueError: + return False + + def main(self, function, args, kwargs): + try: + result = function(*args, **kwargs) + except: + self._exit_event.send_exception(*sys.exc_info()) + self._resolve_links() + raise + else: + self._exit_event.send(result) + self._resolve_links() + + def _resolve_links(self): + # ca and ckw are the curried function arguments + if self._resolving_links: + return + if not self._exit_funcs: + return + self._resolving_links = True + try: + while self._exit_funcs: + f, ca, ckw = self._exit_funcs.popleft() + f(self, *ca, **ckw) + finally: + self._resolving_links = False + + def kill(self, *throw_args): + """Kills the greenthread using :func:`kill`. After being killed + all calls to :meth:`wait` will raise *throw_args* (which default + to :class:`greenlet.GreenletExit`).""" + return kill(self, *throw_args) + + def cancel(self, *throw_args): + """Kills the greenthread using :func:`kill`, but only if it hasn't + already started running. After being canceled, + all calls to :meth:`wait` will raise *throw_args* (which default + to :class:`greenlet.GreenletExit`).""" + return cancel(self, *throw_args) + + +def cancel(g, *throw_args): + """Like :func:`kill`, but only terminates the greenthread if it hasn't + already started execution. If the grenthread has already started + execution, :func:`cancel` has no effect.""" + if not g: + kill(g, *throw_args) + + +def kill(g, *throw_args): + """Terminates the target greenthread by raising an exception into it. + Whatever that greenthread might be doing; be it waiting for I/O or another + primitive, it sees an exception right away. + + By default, this exception is GreenletExit, but a specific exception + may be specified. *throw_args* should be the same as the arguments to + raise; either an exception instance or an exc_info tuple. + + Calling :func:`kill` causes the calling greenthread to cooperatively yield. + """ + if g.dead: + return + hub = hubs.get_hub() + if not g: + # greenlet hasn't started yet and therefore throw won't work + # on its own; semantically we want it to be as though the main + # method never got called + def just_raise(*a, **kw): + if throw_args: + raise throw_args[1].with_traceback(throw_args[2]) + else: + raise greenlet.GreenletExit() + g.run = just_raise + if isinstance(g, GreenThread): + # it's a GreenThread object, so we want to call its main + # method to take advantage of the notification + try: + g.main(just_raise, (), {}) + except: + pass + current = getcurrent() + if current is not hub.greenlet: + # arrange to wake the caller back up immediately + hub.ensure_greenlet() + hub.schedule_call_global(0, current.switch) + g.throw(*throw_args) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/hubs/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/__init__.py new file mode 100644 index 0000000..b1a3e80 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/__init__.py @@ -0,0 +1,188 @@ +import importlib +import inspect +import os +import warnings + +from eventlet import patcher +from eventlet.support import greenlets as greenlet + + +__all__ = ["use_hub", "get_hub", "get_default_hub", "trampoline"] + +threading = patcher.original('threading') +_threadlocal = threading.local() + + +# order is important, get_default_hub returns first available from here +builtin_hub_names = ('epolls', 'kqueue', 'poll', 'selects') +builtin_hub_modules = tuple(importlib.import_module('eventlet.hubs.' + name) for name in builtin_hub_names) + + +class HubError(Exception): + pass + + +def get_default_hub(): + """Select the default hub implementation based on what multiplexing + libraries are installed. The order that the hubs are tried is: + + * epoll + * kqueue + * poll + * select + + .. include:: ../../doc/source/common.txt + .. note :: |internal| + """ + for mod in builtin_hub_modules: + if mod.is_available(): + return mod + + raise HubError('no built-in hubs are available: {}'.format(builtin_hub_modules)) + + +def use_hub(mod=None): + """Use the module *mod*, containing a class called Hub, as the + event hub. Usually not required; the default hub is usually fine. + + `mod` can be an actual hub class, a module, a string, or None. + + If `mod` is a class, use it directly. + If `mod` is a module, use `module.Hub` class + If `mod` is a string and contains either '.' or ':' + then `use_hub` uses 'package.subpackage.module:Class' convention, + otherwise imports `eventlet.hubs.mod`. + If `mod` is None, `use_hub` uses the default hub. + + Only call use_hub during application initialization, + because it resets the hub's state and any existing + timers or listeners will never be resumed. + + These two threadlocal attributes are not part of Eventlet public API: + - `threadlocal.Hub` (capital H) is hub constructor, used when no hub is currently active + - `threadlocal.hub` (lowercase h) is active hub instance + """ + if mod is None: + mod = os.environ.get('EVENTLET_HUB', None) + if mod is None: + mod = get_default_hub() + if hasattr(_threadlocal, 'hub'): + del _threadlocal.hub + + classname = '' + if isinstance(mod, str): + if mod.strip() == "": + raise RuntimeError("Need to specify a hub") + if '.' in mod or ':' in mod: + modulename, _, classname = mod.strip().partition(':') + else: + modulename = 'eventlet.hubs.' + mod + mod = importlib.import_module(modulename) + + if hasattr(mod, 'is_available'): + if not mod.is_available(): + raise Exception('selected hub is not available on this system mod={}'.format(mod)) + else: + msg = '''Please provide `is_available()` function in your custom Eventlet hub {mod}. +It must return bool: whether hub supports current platform. See eventlet/hubs/{{epoll,kqueue}} for example. +'''.format(mod=mod) + warnings.warn(msg, DeprecationWarning, stacklevel=3) + + hubclass = mod + if not inspect.isclass(mod): + hubclass = getattr(mod, classname or 'Hub') + + _threadlocal.Hub = hubclass + + +def get_hub(): + """Get the current event hub singleton object. + + .. note :: |internal| + """ + try: + hub = _threadlocal.hub + except AttributeError: + try: + _threadlocal.Hub + except AttributeError: + use_hub() + hub = _threadlocal.hub = _threadlocal.Hub() + return hub + + +# Lame middle file import because complex dependencies in import graph +from eventlet import timeout + + +def trampoline(fd, read=None, write=None, timeout=None, + timeout_exc=timeout.Timeout, + mark_as_closed=None): + """Suspend the current coroutine until the given socket object or file + descriptor is ready to *read*, ready to *write*, or the specified + *timeout* elapses, depending on arguments specified. + + To wait for *fd* to be ready to read, pass *read* ``=True``; ready to + write, pass *write* ``=True``. To specify a timeout, pass the *timeout* + argument in seconds. + + If the specified *timeout* elapses before the socket is ready to read or + write, *timeout_exc* will be raised instead of ``trampoline()`` + returning normally. + + .. note :: |internal| + """ + t = None + hub = get_hub() + current = greenlet.getcurrent() + if hub.greenlet is current: + raise RuntimeError('do not call blocking functions from the mainloop') + if (read and write): + raise RuntimeError('not allowed to trampoline for reading and writing') + try: + fileno = fd.fileno() + except AttributeError: + fileno = fd + if timeout is not None: + def _timeout(exc): + # This is only useful to insert debugging + current.throw(exc) + t = hub.schedule_call_global(timeout, _timeout, timeout_exc) + try: + if read: + listener = hub.add(hub.READ, fileno, current.switch, current.throw, mark_as_closed) + elif write: + listener = hub.add(hub.WRITE, fileno, current.switch, current.throw, mark_as_closed) + try: + return hub.switch() + finally: + hub.remove(listener) + finally: + if t is not None: + t.cancel() + + +def notify_close(fd): + """ + A particular file descriptor has been explicitly closed. Register for any + waiting listeners to be notified on the next run loop. + """ + hub = get_hub() + hub.notify_close(fd) + + +def notify_opened(fd): + """ + Some file descriptors may be closed 'silently' - that is, by the garbage + collector, by an external library, etc. When the OS returns a file descriptor + from an open call (or something similar), this may be the only indication we + have that the FD has been closed and then recycled. + We let the hub know that the old file descriptor is dead; any stuck listeners + will be disabled and notified in turn. + """ + hub = get_hub() + hub.mark_as_reopened(fd) + + +class IOClosed(IOError): + pass diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/hubs/asyncio.py b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/asyncio.py new file mode 100644 index 0000000..2b9b7e5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/asyncio.py @@ -0,0 +1,174 @@ +""" +Asyncio-based hub, originally implemented by Miguel Grinberg. +""" + +# The various modules involved in asyncio need to call the original, unpatched +# standard library APIs to work: socket, select, threading, and so on. We +# therefore don't import them on the module level, since that would involve +# their imports getting patched, and instead delay importing them as much as +# possible. Then, we do a little song and dance in Hub.__init__ below so that +# when they're imported they import the original modules (select, socket, etc) +# rather than the patched ones. + +import os +import sys + +from eventlet.hubs import hub +from eventlet.patcher import _unmonkey_patch_asyncio_all + + +def is_available(): + """ + Indicate whether this hub is available, since some hubs are + platform-specific. + + Python always has asyncio, so this is always ``True``. + """ + return True + + +class Hub(hub.BaseHub): + """An Eventlet hub implementation on top of an asyncio event loop.""" + + def __init__(self): + super().__init__() + + # Pre-emptively make sure we're using the right modules: + _unmonkey_patch_asyncio_all() + + # The presumption is that eventlet is driving the event loop, so we + # want a new one we control. + import asyncio + + self.loop = asyncio.new_event_loop() + asyncio.set_event_loop(self.loop) + self.sleep_event = asyncio.Event() + + import asyncio.events + if hasattr(asyncio.events, "on_fork"): + # Allow post-fork() child to continue using the same event loop. + # This is a terrible idea. + asyncio.events.on_fork.__code__ = (lambda: None).__code__ + else: + # On Python 3.9-3.11, there's a thread local we need to reset. + # Also a terrible idea. + def re_register_loop(loop=self.loop): + asyncio.events._set_running_loop(loop) + + os.register_at_fork(after_in_child=re_register_loop) + + def add_timer(self, timer): + """ + Register a ``Timer``. + + Typically not called directly by users. + """ + super().add_timer(timer) + self.sleep_event.set() + + def _file_cb(self, cb, fileno): + """ + Callback called by ``asyncio`` when a file descriptor has an event. + """ + try: + cb(fileno) + except self.SYSTEM_EXCEPTIONS: + raise + except: + self.squelch_exception(fileno, sys.exc_info()) + self.sleep_event.set() + + def add(self, evtype, fileno, cb, tb, mark_as_closed): + """ + Add a file descriptor of given event type to the ``Hub``. See the + superclass for details. + + Typically not called directly by users. + """ + try: + os.fstat(fileno) + except OSError: + raise ValueError("Invalid file descriptor") + already_listening = self.listeners[evtype].get(fileno) is not None + listener = super().add(evtype, fileno, cb, tb, mark_as_closed) + if not already_listening: + if evtype == hub.READ: + self.loop.add_reader(fileno, self._file_cb, cb, fileno) + else: + self.loop.add_writer(fileno, self._file_cb, cb, fileno) + return listener + + def remove(self, listener): + """ + Remove a listener from the ``Hub``. See the superclass for details. + + Typically not called directly by users. + """ + super().remove(listener) + evtype = listener.evtype + fileno = listener.fileno + if not self.listeners[evtype].get(fileno): + if evtype == hub.READ: + self.loop.remove_reader(fileno) + else: + self.loop.remove_writer(fileno) + + def remove_descriptor(self, fileno): + """ + Remove a file descriptor from the ``asyncio`` loop. + + Typically not called directly by users. + """ + have_read = self.listeners[hub.READ].get(fileno) + have_write = self.listeners[hub.WRITE].get(fileno) + super().remove_descriptor(fileno) + if have_read: + self.loop.remove_reader(fileno) + if have_write: + self.loop.remove_writer(fileno) + + def run(self, *a, **kw): + """ + Start the ``Hub`` running. See the superclass for details. + """ + import asyncio + + async def async_run(): + if self.running: + raise RuntimeError("Already running!") + try: + self.running = True + self.stopping = False + while not self.stopping: + while self.closed: + # We ditch all of these first. + self.close_one() + self.prepare_timers() + if self.debug_blocking: + self.block_detect_pre() + self.fire_timers(self.clock()) + if self.debug_blocking: + self.block_detect_post() + self.prepare_timers() + wakeup_when = self.sleep_until() + if wakeup_when is None: + sleep_time = self.default_sleep() + else: + sleep_time = wakeup_when - self.clock() + if sleep_time > 0: + try: + await asyncio.wait_for(self.sleep_event.wait(), sleep_time) + except asyncio.TimeoutError: + pass + self.sleep_event.clear() + else: + await asyncio.sleep(0) + else: + self.timers_canceled = 0 + del self.timers[:] + del self.next_timers[:] + finally: + self.running = False + self.stopping = False + + self.loop.run_until_complete(async_run()) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/hubs/epolls.py b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/epolls.py new file mode 100644 index 0000000..770c18d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/epolls.py @@ -0,0 +1,31 @@ +import errno +from eventlet import patcher, support +from eventlet.hubs import hub, poll +select = patcher.original('select') + + +def is_available(): + return hasattr(select, 'epoll') + + +# NOTE: we rely on the fact that the epoll flag constants +# are identical in value to the poll constants +class Hub(poll.Hub): + def __init__(self, clock=None): + super().__init__(clock=clock) + self.poll = select.epoll() + + def add(self, evtype, fileno, cb, tb, mac): + oldlisteners = bool(self.listeners[self.READ].get(fileno) or + self.listeners[self.WRITE].get(fileno)) + # not super() to avoid double register() + listener = hub.BaseHub.add(self, evtype, fileno, cb, tb, mac) + try: + self.register(fileno, new=not oldlisteners) + except OSError as ex: # ignore EEXIST, #80 + if support.get_errno(ex) != errno.EEXIST: + raise + return listener + + def do_poll(self, seconds): + return self.poll.poll(seconds) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/hubs/hub.py b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/hub.py new file mode 100644 index 0000000..abeee6c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/hub.py @@ -0,0 +1,495 @@ +import errno +import heapq +import math +import signal +import sys +import traceback + +arm_alarm = None +if hasattr(signal, 'setitimer'): + def alarm_itimer(seconds): + signal.setitimer(signal.ITIMER_REAL, seconds) + arm_alarm = alarm_itimer +else: + try: + import itimer + arm_alarm = itimer.alarm + except ImportError: + def alarm_signal(seconds): + signal.alarm(math.ceil(seconds)) + arm_alarm = alarm_signal + +import eventlet.hubs +from eventlet.hubs import timer +from eventlet.support import greenlets as greenlet +try: + from monotonic import monotonic +except ImportError: + from time import monotonic + +g_prevent_multiple_readers = True + +READ = "read" +WRITE = "write" + + +def closed_callback(fileno): + """ Used to de-fang a callback that may be triggered by a loop in BaseHub.wait + """ + # No-op. + pass + + +class FdListener: + + def __init__(self, evtype, fileno, cb, tb, mark_as_closed): + """ The following are required: + cb - the standard callback, which will switch into the + listening greenlet to indicate that the event waited upon + is ready + tb - a 'throwback'. This is typically greenlet.throw, used + to raise a signal into the target greenlet indicating that + an event was obsoleted by its underlying filehandle being + repurposed. + mark_as_closed - if any listener is obsoleted, this is called + (in the context of some other client greenlet) to alert + underlying filehandle-wrapping objects that they've been + closed. + """ + assert (evtype is READ or evtype is WRITE) + self.evtype = evtype + self.fileno = fileno + self.cb = cb + self.tb = tb + self.mark_as_closed = mark_as_closed + self.spent = False + self.greenlet = greenlet.getcurrent() + + def __repr__(self): + return "%s(%r, %r, %r, %r)" % (type(self).__name__, self.evtype, self.fileno, + self.cb, self.tb) + __str__ = __repr__ + + def defang(self): + self.cb = closed_callback + if self.mark_as_closed is not None: + self.mark_as_closed() + self.spent = True + + +noop = FdListener(READ, 0, lambda x: None, lambda x: None, None) + + +# in debug mode, track the call site that created the listener + + +class DebugListener(FdListener): + + def __init__(self, evtype, fileno, cb, tb, mark_as_closed): + self.where_called = traceback.format_stack() + self.greenlet = greenlet.getcurrent() + super().__init__(evtype, fileno, cb, tb, mark_as_closed) + + def __repr__(self): + return "DebugListener(%r, %r, %r, %r, %r, %r)\n%sEndDebugFdListener" % ( + self.evtype, + self.fileno, + self.cb, + self.tb, + self.mark_as_closed, + self.greenlet, + ''.join(self.where_called)) + __str__ = __repr__ + + +def alarm_handler(signum, frame): + import inspect + raise RuntimeError("Blocking detector ALARMED at" + str(inspect.getframeinfo(frame))) + + +class BaseHub: + """ Base hub class for easing the implementation of subclasses that are + specific to a particular underlying event architecture. """ + + SYSTEM_EXCEPTIONS = (KeyboardInterrupt, SystemExit) + + READ = READ + WRITE = WRITE + + def __init__(self, clock=None): + self.listeners = {READ: {}, WRITE: {}} + self.secondaries = {READ: {}, WRITE: {}} + self.closed = [] + + if clock is None: + clock = monotonic + self.clock = clock + + self.greenlet = greenlet.greenlet(self.run) + self.stopping = False + self.running = False + self.timers = [] + self.next_timers = [] + self.lclass = FdListener + self.timers_canceled = 0 + self.debug_exceptions = True + self.debug_blocking = False + self.debug_blocking_resolution = 1 + + def block_detect_pre(self): + # shortest alarm we can possibly raise is one second + tmp = signal.signal(signal.SIGALRM, alarm_handler) + if tmp != alarm_handler: + self._old_signal_handler = tmp + + arm_alarm(self.debug_blocking_resolution) + + def block_detect_post(self): + if (hasattr(self, "_old_signal_handler") and + self._old_signal_handler): + signal.signal(signal.SIGALRM, self._old_signal_handler) + signal.alarm(0) + + def add(self, evtype, fileno, cb, tb, mark_as_closed): + """ Signals an intent to or write a particular file descriptor. + + The *evtype* argument is either the constant READ or WRITE. + + The *fileno* argument is the file number of the file of interest. + + The *cb* argument is the callback which will be called when the file + is ready for reading/writing. + + The *tb* argument is the throwback used to signal (into the greenlet) + that the file was closed. + + The *mark_as_closed* is used in the context of the event hub to + prepare a Python object as being closed, pre-empting further + close operations from accidentally shutting down the wrong OS thread. + """ + listener = self.lclass(evtype, fileno, cb, tb, mark_as_closed) + bucket = self.listeners[evtype] + if fileno in bucket: + if g_prevent_multiple_readers: + raise RuntimeError( + "Second simultaneous %s on fileno %s " + "detected. Unless you really know what you're doing, " + "make sure that only one greenthread can %s any " + "particular socket. Consider using a pools.Pool. " + "If you do know what you're doing and want to disable " + "this error, call " + "eventlet.debug.hub_prevent_multiple_readers(False) - MY THREAD=%s; " + "THAT THREAD=%s" % ( + evtype, fileno, evtype, cb, bucket[fileno])) + # store off the second listener in another structure + self.secondaries[evtype].setdefault(fileno, []).append(listener) + else: + bucket[fileno] = listener + return listener + + def _obsolete(self, fileno): + """ We've received an indication that 'fileno' has been obsoleted. + Any current listeners must be defanged, and notifications to + their greenlets queued up to send. + """ + found = False + for evtype, bucket in self.secondaries.items(): + if fileno in bucket: + for listener in bucket[fileno]: + found = True + self.closed.append(listener) + listener.defang() + del bucket[fileno] + + # For the primary listeners, we actually need to call remove, + # which may modify the underlying OS polling objects. + for evtype, bucket in self.listeners.items(): + if fileno in bucket: + listener = bucket[fileno] + found = True + self.closed.append(listener) + self.remove(listener) + listener.defang() + + return found + + def notify_close(self, fileno): + """ We might want to do something when a fileno is closed. + However, currently it suffices to obsolete listeners only + when we detect an old fileno being recycled, on open. + """ + pass + + def remove(self, listener): + if listener.spent: + # trampoline may trigger this in its finally section. + return + + fileno = listener.fileno + evtype = listener.evtype + if listener is self.listeners[evtype][fileno]: + del self.listeners[evtype][fileno] + # migrate a secondary listener to be the primary listener + if fileno in self.secondaries[evtype]: + sec = self.secondaries[evtype][fileno] + if sec: + self.listeners[evtype][fileno] = sec.pop(0) + if not sec: + del self.secondaries[evtype][fileno] + else: + self.secondaries[evtype][fileno].remove(listener) + if not self.secondaries[evtype][fileno]: + del self.secondaries[evtype][fileno] + + def mark_as_reopened(self, fileno): + """ If a file descriptor is returned by the OS as the result of some + open call (or equivalent), that signals that it might be being + recycled. + + Catch the case where the fd was previously in use. + """ + self._obsolete(fileno) + + def remove_descriptor(self, fileno): + """ Completely remove all listeners for this fileno. For internal use + only.""" + # gather any listeners we have + listeners = [] + listeners.append(self.listeners[READ].get(fileno, noop)) + listeners.append(self.listeners[WRITE].get(fileno, noop)) + listeners.extend(self.secondaries[READ].get(fileno, ())) + listeners.extend(self.secondaries[WRITE].get(fileno, ())) + for listener in listeners: + try: + # listener.cb may want to remove(listener) + listener.cb(fileno) + except Exception: + self.squelch_generic_exception(sys.exc_info()) + # NOW this fileno is now dead to all + self.listeners[READ].pop(fileno, None) + self.listeners[WRITE].pop(fileno, None) + self.secondaries[READ].pop(fileno, None) + self.secondaries[WRITE].pop(fileno, None) + + def close_one(self): + """ Triggered from the main run loop. If a listener's underlying FD was + closed somehow, throw an exception back to the trampoline, which should + be able to manage it appropriately. + """ + listener = self.closed.pop() + if not listener.greenlet.dead: + # There's no point signalling a greenlet that's already dead. + listener.tb(eventlet.hubs.IOClosed(errno.ENOTCONN, "Operation on closed file")) + + def ensure_greenlet(self): + if self.greenlet.dead: + # create new greenlet sharing same parent as original + new = greenlet.greenlet(self.run, self.greenlet.parent) + # need to assign as parent of old greenlet + # for those greenlets that are currently + # children of the dead hub and may subsequently + # exit without further switching to hub. + self.greenlet.parent = new + self.greenlet = new + + def switch(self): + cur = greenlet.getcurrent() + assert cur is not self.greenlet, 'Cannot switch to MAINLOOP from MAINLOOP' + switch_out = getattr(cur, 'switch_out', None) + if switch_out is not None: + try: + switch_out() + except: + self.squelch_generic_exception(sys.exc_info()) + self.ensure_greenlet() + try: + if self.greenlet.parent is not cur: + cur.parent = self.greenlet + except ValueError: + pass # gets raised if there is a greenlet parent cycle + return self.greenlet.switch() + + def squelch_exception(self, fileno, exc_info): + traceback.print_exception(*exc_info) + sys.stderr.write("Removing descriptor: %r\n" % (fileno,)) + sys.stderr.flush() + try: + self.remove_descriptor(fileno) + except Exception as e: + sys.stderr.write("Exception while removing descriptor! %r\n" % (e,)) + sys.stderr.flush() + + def wait(self, seconds=None): + raise NotImplementedError("Implement this in a subclass") + + def default_sleep(self): + return 60.0 + + def sleep_until(self): + t = self.timers + if not t: + return None + return t[0][0] + + def run(self, *a, **kw): + """Run the runloop until abort is called. + """ + # accept and discard variable arguments because they will be + # supplied if other greenlets have run and exited before the + # hub's greenlet gets a chance to run + if self.running: + raise RuntimeError("Already running!") + try: + self.running = True + self.stopping = False + while not self.stopping: + while self.closed: + # We ditch all of these first. + self.close_one() + self.prepare_timers() + if self.debug_blocking: + self.block_detect_pre() + self.fire_timers(self.clock()) + if self.debug_blocking: + self.block_detect_post() + self.prepare_timers() + wakeup_when = self.sleep_until() + if wakeup_when is None: + sleep_time = self.default_sleep() + else: + sleep_time = wakeup_when - self.clock() + if sleep_time > 0: + self.wait(sleep_time) + else: + self.wait(0) + else: + self.timers_canceled = 0 + del self.timers[:] + del self.next_timers[:] + finally: + self.running = False + self.stopping = False + + def abort(self, wait=False): + """Stop the runloop. If run is executing, it will exit after + completing the next runloop iteration. + + Set *wait* to True to cause abort to switch to the hub immediately and + wait until it's finished processing. Waiting for the hub will only + work from the main greenthread; all other greenthreads will become + unreachable. + """ + if self.running: + self.stopping = True + if wait: + assert self.greenlet is not greenlet.getcurrent( + ), "Can't abort with wait from inside the hub's greenlet." + # schedule an immediate timer just so the hub doesn't sleep + self.schedule_call_global(0, lambda: None) + # switch to it; when done the hub will switch back to its parent, + # the main greenlet + self.switch() + + def squelch_generic_exception(self, exc_info): + if self.debug_exceptions: + traceback.print_exception(*exc_info) + sys.stderr.flush() + + def squelch_timer_exception(self, timer, exc_info): + if self.debug_exceptions: + traceback.print_exception(*exc_info) + sys.stderr.flush() + + def add_timer(self, timer): + scheduled_time = self.clock() + timer.seconds + self.next_timers.append((scheduled_time, timer)) + return scheduled_time + + def timer_canceled(self, timer): + self.timers_canceled += 1 + len_timers = len(self.timers) + len(self.next_timers) + if len_timers > 1000 and len_timers / 2 <= self.timers_canceled: + self.timers_canceled = 0 + self.timers = [t for t in self.timers if not t[1].called] + self.next_timers = [t for t in self.next_timers if not t[1].called] + heapq.heapify(self.timers) + + def prepare_timers(self): + heappush = heapq.heappush + t = self.timers + for item in self.next_timers: + if item[1].called: + self.timers_canceled -= 1 + else: + heappush(t, item) + del self.next_timers[:] + + def schedule_call_local(self, seconds, cb, *args, **kw): + """Schedule a callable to be called after 'seconds' seconds have + elapsed. Cancel the timer if greenlet has exited. + seconds: The number of seconds to wait. + cb: The callable to call after the given time. + *args: Arguments to pass to the callable when called. + **kw: Keyword arguments to pass to the callable when called. + """ + t = timer.LocalTimer(seconds, cb, *args, **kw) + self.add_timer(t) + return t + + def schedule_call_global(self, seconds, cb, *args, **kw): + """Schedule a callable to be called after 'seconds' seconds have + elapsed. The timer will NOT be canceled if the current greenlet has + exited before the timer fires. + seconds: The number of seconds to wait. + cb: The callable to call after the given time. + *args: Arguments to pass to the callable when called. + **kw: Keyword arguments to pass to the callable when called. + """ + t = timer.Timer(seconds, cb, *args, **kw) + self.add_timer(t) + return t + + def fire_timers(self, when): + t = self.timers + heappop = heapq.heappop + + while t: + next = t[0] + + exp = next[0] + timer = next[1] + + if when < exp: + break + + heappop(t) + + try: + if timer.called: + self.timers_canceled -= 1 + else: + timer() + except self.SYSTEM_EXCEPTIONS: + raise + except: + self.squelch_timer_exception(timer, sys.exc_info()) + + # for debugging: + + def get_readers(self): + return self.listeners[READ].values() + + def get_writers(self): + return self.listeners[WRITE].values() + + def get_timers_count(hub): + return len(hub.timers) + len(hub.next_timers) + + def set_debug_listeners(self, value): + if value: + self.lclass = DebugListener + else: + self.lclass = FdListener + + def set_timer_exceptions(self, value): + self.debug_exceptions = value diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/hubs/kqueue.py b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/kqueue.py new file mode 100644 index 0000000..9502576 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/kqueue.py @@ -0,0 +1,110 @@ +import os +import sys +from eventlet import patcher, support +from eventlet.hubs import hub +select = patcher.original('select') +time = patcher.original('time') + + +def is_available(): + return hasattr(select, 'kqueue') + + +class Hub(hub.BaseHub): + MAX_EVENTS = 100 + + def __init__(self, clock=None): + self.FILTERS = { + hub.READ: select.KQ_FILTER_READ, + hub.WRITE: select.KQ_FILTER_WRITE, + } + super().__init__(clock) + self._events = {} + self._init_kqueue() + + def _init_kqueue(self): + self.kqueue = select.kqueue() + self._pid = os.getpid() + + def _reinit_kqueue(self): + self.kqueue.close() + self._init_kqueue() + events = [e for i in self._events.values() + for e in i.values()] + self.kqueue.control(events, 0, 0) + + def _control(self, events, max_events, timeout): + try: + return self.kqueue.control(events, max_events, timeout) + except OSError: + # have we forked? + if os.getpid() != self._pid: + self._reinit_kqueue() + return self.kqueue.control(events, max_events, timeout) + raise + + def add(self, evtype, fileno, cb, tb, mac): + listener = super().add(evtype, fileno, cb, tb, mac) + events = self._events.setdefault(fileno, {}) + if evtype not in events: + try: + event = select.kevent(fileno, self.FILTERS.get(evtype), select.KQ_EV_ADD) + self._control([event], 0, 0) + events[evtype] = event + except ValueError: + super().remove(listener) + raise + return listener + + def _delete_events(self, events): + del_events = [ + select.kevent(e.ident, e.filter, select.KQ_EV_DELETE) + for e in events + ] + self._control(del_events, 0, 0) + + def remove(self, listener): + super().remove(listener) + evtype = listener.evtype + fileno = listener.fileno + if not self.listeners[evtype].get(fileno): + event = self._events[fileno].pop(evtype, None) + if event is None: + return + try: + self._delete_events((event,)) + except OSError: + pass + + def remove_descriptor(self, fileno): + super().remove_descriptor(fileno) + try: + events = self._events.pop(fileno).values() + self._delete_events(events) + except KeyError: + pass + except OSError: + pass + + def wait(self, seconds=None): + readers = self.listeners[self.READ] + writers = self.listeners[self.WRITE] + + if not readers and not writers: + if seconds: + time.sleep(seconds) + return + result = self._control([], self.MAX_EVENTS, seconds) + SYSTEM_EXCEPTIONS = self.SYSTEM_EXCEPTIONS + for event in result: + fileno = event.ident + evfilt = event.filter + try: + if evfilt == select.KQ_FILTER_READ: + readers.get(fileno, hub.noop).cb(fileno) + if evfilt == select.KQ_FILTER_WRITE: + writers.get(fileno, hub.noop).cb(fileno) + except SYSTEM_EXCEPTIONS: + raise + except: + self.squelch_exception(fileno, sys.exc_info()) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/hubs/poll.py b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/poll.py new file mode 100644 index 0000000..0984214 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/poll.py @@ -0,0 +1,118 @@ +import errno +import sys + +from eventlet import patcher, support +from eventlet.hubs import hub +select = patcher.original('select') +time = patcher.original('time') + + +def is_available(): + return hasattr(select, 'poll') + + +class Hub(hub.BaseHub): + def __init__(self, clock=None): + super().__init__(clock) + self.EXC_MASK = select.POLLERR | select.POLLHUP + self.READ_MASK = select.POLLIN | select.POLLPRI + self.WRITE_MASK = select.POLLOUT + self.poll = select.poll() + + def add(self, evtype, fileno, cb, tb, mac): + listener = super().add(evtype, fileno, cb, tb, mac) + self.register(fileno, new=True) + return listener + + def remove(self, listener): + super().remove(listener) + self.register(listener.fileno) + + def register(self, fileno, new=False): + mask = 0 + if self.listeners[self.READ].get(fileno): + mask |= self.READ_MASK | self.EXC_MASK + if self.listeners[self.WRITE].get(fileno): + mask |= self.WRITE_MASK | self.EXC_MASK + try: + if mask: + if new: + self.poll.register(fileno, mask) + else: + try: + self.poll.modify(fileno, mask) + except OSError: + self.poll.register(fileno, mask) + else: + try: + self.poll.unregister(fileno) + except (KeyError, OSError): + # raised if we try to remove a fileno that was + # already removed/invalid + pass + except ValueError: + # fileno is bad, issue 74 + self.remove_descriptor(fileno) + raise + + def remove_descriptor(self, fileno): + super().remove_descriptor(fileno) + try: + self.poll.unregister(fileno) + except (KeyError, ValueError, OSError): + # raised if we try to remove a fileno that was + # already removed/invalid + pass + + def do_poll(self, seconds): + # poll.poll expects integral milliseconds + return self.poll.poll(int(seconds * 1000.0)) + + def wait(self, seconds=None): + readers = self.listeners[self.READ] + writers = self.listeners[self.WRITE] + + if not readers and not writers: + if seconds: + time.sleep(seconds) + return + try: + presult = self.do_poll(seconds) + except OSError as e: + if support.get_errno(e) == errno.EINTR: + return + raise + SYSTEM_EXCEPTIONS = self.SYSTEM_EXCEPTIONS + + if self.debug_blocking: + self.block_detect_pre() + + # Accumulate the listeners to call back to prior to + # triggering any of them. This is to keep the set + # of callbacks in sync with the events we've just + # polled for. It prevents one handler from invalidating + # another. + callbacks = set() + noop = hub.noop # shave getattr + for fileno, event in presult: + if event & self.READ_MASK: + callbacks.add((readers.get(fileno, noop), fileno)) + if event & self.WRITE_MASK: + callbacks.add((writers.get(fileno, noop), fileno)) + if event & select.POLLNVAL: + self.remove_descriptor(fileno) + continue + if event & self.EXC_MASK: + callbacks.add((readers.get(fileno, noop), fileno)) + callbacks.add((writers.get(fileno, noop), fileno)) + + for listener, fileno in callbacks: + try: + listener.cb(fileno) + except SYSTEM_EXCEPTIONS: + raise + except: + self.squelch_exception(fileno, sys.exc_info()) + + if self.debug_blocking: + self.block_detect_post() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/hubs/pyevent.py b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/pyevent.py new file mode 100644 index 0000000..0802243 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/pyevent.py @@ -0,0 +1,4 @@ +raise ImportError( + "Eventlet pyevent hub was removed because it was not maintained." + " Try version 0.22.1 or older. Sorry for the inconvenience." +) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/hubs/selects.py b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/selects.py new file mode 100644 index 0000000..b6cf129 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/selects.py @@ -0,0 +1,63 @@ +import errno +import sys +from eventlet import patcher, support +from eventlet.hubs import hub +select = patcher.original('select') +time = patcher.original('time') + +try: + BAD_SOCK = {errno.EBADF, errno.WSAENOTSOCK} +except AttributeError: + BAD_SOCK = {errno.EBADF} + + +def is_available(): + return hasattr(select, 'select') + + +class Hub(hub.BaseHub): + def _remove_bad_fds(self): + """ Iterate through fds, removing the ones that are bad per the + operating system. + """ + all_fds = list(self.listeners[self.READ]) + list(self.listeners[self.WRITE]) + for fd in all_fds: + try: + select.select([fd], [], [], 0) + except OSError as e: + if support.get_errno(e) in BAD_SOCK: + self.remove_descriptor(fd) + + def wait(self, seconds=None): + readers = self.listeners[self.READ] + writers = self.listeners[self.WRITE] + if not readers and not writers: + if seconds: + time.sleep(seconds) + return + reader_fds = list(readers) + writer_fds = list(writers) + all_fds = reader_fds + writer_fds + try: + r, w, er = select.select(reader_fds, writer_fds, all_fds, seconds) + except OSError as e: + if support.get_errno(e) == errno.EINTR: + return + elif support.get_errno(e) in BAD_SOCK: + self._remove_bad_fds() + return + else: + raise + + for fileno in er: + readers.get(fileno, hub.noop).cb(fileno) + writers.get(fileno, hub.noop).cb(fileno) + + for listeners, events in ((readers, r), (writers, w)): + for fileno in events: + try: + listeners.get(fileno, hub.noop).cb(fileno) + except self.SYSTEM_EXCEPTIONS: + raise + except: + self.squelch_exception(fileno, sys.exc_info()) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/hubs/timer.py b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/timer.py new file mode 100644 index 0000000..2e3fd95 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/hubs/timer.py @@ -0,0 +1,106 @@ +import traceback + +import eventlet.hubs +from eventlet.support import greenlets as greenlet +import io + +""" If true, captures a stack trace for each timer when constructed. This is +useful for debugging leaking timers, to find out where the timer was set up. """ +_g_debug = False + + +class Timer: + def __init__(self, seconds, cb, *args, **kw): + """Create a timer. + seconds: The minimum number of seconds to wait before calling + cb: The callback to call when the timer has expired + *args: The arguments to pass to cb + **kw: The keyword arguments to pass to cb + + This timer will not be run unless it is scheduled in a runloop by + calling timer.schedule() or runloop.add_timer(timer). + """ + self.seconds = seconds + self.tpl = cb, args, kw + self.called = False + if _g_debug: + self.traceback = io.StringIO() + traceback.print_stack(file=self.traceback) + + @property + def pending(self): + return not self.called + + def __repr__(self): + secs = getattr(self, 'seconds', None) + cb, args, kw = getattr(self, 'tpl', (None, None, None)) + retval = "Timer(%s, %s, *%s, **%s)" % ( + secs, cb, args, kw) + if _g_debug and hasattr(self, 'traceback'): + retval += '\n' + self.traceback.getvalue() + return retval + + def copy(self): + cb, args, kw = self.tpl + return self.__class__(self.seconds, cb, *args, **kw) + + def schedule(self): + """Schedule this timer to run in the current runloop. + """ + self.called = False + self.scheduled_time = eventlet.hubs.get_hub().add_timer(self) + return self + + def __call__(self, *args): + if not self.called: + self.called = True + cb, args, kw = self.tpl + try: + cb(*args, **kw) + finally: + try: + del self.tpl + except AttributeError: + pass + + def cancel(self): + """Prevent this timer from being called. If the timer has already + been called or canceled, has no effect. + """ + if not self.called: + self.called = True + eventlet.hubs.get_hub().timer_canceled(self) + try: + del self.tpl + except AttributeError: + pass + + # No default ordering in 3.x. heapq uses < + # FIXME should full set be added? + def __lt__(self, other): + return id(self) < id(other) + + +class LocalTimer(Timer): + + def __init__(self, *args, **kwargs): + self.greenlet = greenlet.getcurrent() + Timer.__init__(self, *args, **kwargs) + + @property + def pending(self): + if self.greenlet is None or self.greenlet.dead: + return False + return not self.called + + def __call__(self, *args): + if not self.called: + self.called = True + if self.greenlet is not None and self.greenlet.dead: + return + cb, args, kw = self.tpl + cb(*args, **kw) + + def cancel(self): + self.greenlet = None + Timer.cancel(self) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/lock.py b/netdeploy/lib/python3.11/site-packages/eventlet/lock.py new file mode 100644 index 0000000..4b21e0b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/lock.py @@ -0,0 +1,37 @@ +from eventlet import hubs +from eventlet.semaphore import Semaphore + + +class Lock(Semaphore): + + """A lock. + This is API-compatible with :class:`threading.Lock`. + + It is a context manager, and thus can be used in a with block:: + + lock = Lock() + with lock: + do_some_stuff() + """ + + def release(self, blocking=True): + """Modify behaviour vs :class:`Semaphore` to raise a RuntimeError + exception if the value is greater than zero. This corrects behaviour + to realign with :class:`threading.Lock`. + """ + if self.counter > 0: + raise RuntimeError("release unlocked lock") + + # Consciously *do not* call super().release(), but instead inline + # Semaphore.release() here. We've seen issues with logging._lock + # deadlocking because garbage collection happened to run mid-release + # and eliminating the extra stack frame should help prevent that. + # See https://github.com/eventlet/eventlet/issues/742 + self.counter += 1 + if self._waiters: + hubs.get_hub().schedule_call_global(0, self._do_acquire) + return True + + def _at_fork_reinit(self): + self.counter = 1 + self._waiters.clear() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/patcher.py b/netdeploy/lib/python3.11/site-packages/eventlet/patcher.py new file mode 100644 index 0000000..12d8069 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/patcher.py @@ -0,0 +1,773 @@ +from __future__ import annotations + +try: + import _imp as imp +except ImportError: + import imp +import importlib +import sys + +try: + # Only for this purpose, it's irrelevant if `os` was already patched. + # https://github.com/eventlet/eventlet/pull/661 + from os import register_at_fork +except ImportError: + register_at_fork = None + +import eventlet + + +__all__ = ["inject", "import_patched", "monkey_patch", "is_monkey_patched"] + +__exclude = {"__builtins__", "__file__", "__name__"} + + +class SysModulesSaver: + """Class that captures some subset of the current state of + sys.modules. Pass in an iterator of module names to the + constructor.""" + + def __init__(self, module_names=()): + self._saved = {} + imp.acquire_lock() + self.save(*module_names) + + def save(self, *module_names): + """Saves the named modules to the object.""" + for modname in module_names: + self._saved[modname] = sys.modules.get(modname, None) + + def restore(self): + """Restores the modules that the saver knows about into + sys.modules. + """ + try: + for modname, mod in self._saved.items(): + if mod is not None: + sys.modules[modname] = mod + else: + try: + del sys.modules[modname] + except KeyError: + pass + finally: + imp.release_lock() + + +def inject(module_name, new_globals, *additional_modules): + """Base method for "injecting" greened modules into an imported module. It + imports the module specified in *module_name*, arranging things so + that the already-imported modules in *additional_modules* are used when + *module_name* makes its imports. + + **Note:** This function does not create or change any sys.modules item, so + if your greened module use code like 'sys.modules["your_module_name"]', you + need to update sys.modules by yourself. + + *new_globals* is either None or a globals dictionary that gets populated + with the contents of the *module_name* module. This is useful when creating + a "green" version of some other module. + + *additional_modules* should be a collection of two-element tuples, of the + form (, ). If it's not specified, a default selection of + name/module pairs is used, which should cover all use cases but may be + slower because there are inevitably redundant or unnecessary imports. + """ + patched_name = "__patched_module_" + module_name + if patched_name in sys.modules: + # returning already-patched module so as not to destroy existing + # references to patched modules + return sys.modules[patched_name] + + if not additional_modules: + # supply some defaults + additional_modules = ( + _green_os_modules() + + _green_select_modules() + + _green_socket_modules() + + _green_thread_modules() + + _green_time_modules() + ) + # _green_MySQLdb()) # enable this after a short baking-in period + + # after this we are gonna screw with sys.modules, so capture the + # state of all the modules we're going to mess with, and lock + saver = SysModulesSaver([name for name, m in additional_modules]) + saver.save(module_name) + + # Cover the target modules so that when you import the module it + # sees only the patched versions + for name, mod in additional_modules: + sys.modules[name] = mod + + # Remove the old module from sys.modules and reimport it while + # the specified modules are in place + sys.modules.pop(module_name, None) + # Also remove sub modules and reimport. Use copy the keys to list + # because of the pop operations will change the content of sys.modules + # within th loop + for imported_module_name in list(sys.modules.keys()): + if imported_module_name.startswith(module_name + "."): + sys.modules.pop(imported_module_name, None) + try: + module = __import__(module_name, {}, {}, module_name.split(".")[:-1]) + + if new_globals is not None: + # Update the given globals dictionary with everything from this new module + for name in dir(module): + if name not in __exclude: + new_globals[name] = getattr(module, name) + + # Keep a reference to the new module to prevent it from dying + sys.modules[patched_name] = module + finally: + saver.restore() # Put the original modules back + + return module + + +def import_patched(module_name, *additional_modules, **kw_additional_modules): + """Imports a module in a way that ensures that the module uses "green" + versions of the standard library modules, so that everything works + nonblockingly. + + The only required argument is the name of the module to be imported. + """ + return inject( + module_name, None, *additional_modules + tuple(kw_additional_modules.items()) + ) + + +def patch_function(func, *additional_modules): + """Decorator that returns a version of the function that patches + some modules for the duration of the function call. This is + deeply gross and should only be used for functions that import + network libraries within their function bodies that there is no + way of getting around.""" + if not additional_modules: + # supply some defaults + additional_modules = ( + _green_os_modules() + + _green_select_modules() + + _green_socket_modules() + + _green_thread_modules() + + _green_time_modules() + ) + + def patched(*args, **kw): + saver = SysModulesSaver() + for name, mod in additional_modules: + saver.save(name) + sys.modules[name] = mod + try: + return func(*args, **kw) + finally: + saver.restore() + + return patched + + +def _original_patch_function(func, *module_names): + """Kind of the contrapositive of patch_function: decorates a + function such that when it's called, sys.modules is populated only + with the unpatched versions of the specified modules. Unlike + patch_function, only the names of the modules need be supplied, + and there are no defaults. This is a gross hack; tell your kids not + to import inside function bodies!""" + + def patched(*args, **kw): + saver = SysModulesSaver(module_names) + for name in module_names: + sys.modules[name] = original(name) + try: + return func(*args, **kw) + finally: + saver.restore() + + return patched + + +def original(modname): + """This returns an unpatched version of a module; this is useful for + Eventlet itself (i.e. tpool).""" + # note that it's not necessary to temporarily install unpatched + # versions of all patchable modules during the import of the + # module; this is because none of them import each other, except + # for threading which imports thread + original_name = "__original_module_" + modname + if original_name in sys.modules: + return sys.modules.get(original_name) + + # re-import the "pure" module and store it in the global _originals + # dict; be sure to restore whatever module had that name already + saver = SysModulesSaver((modname,)) + sys.modules.pop(modname, None) + # some rudimentary dependency checking -- fortunately the modules + # we're working on don't have many dependencies so we can just do + # some special-casing here + deps = {"threading": "_thread", "queue": "threading"} + if modname in deps: + dependency = deps[modname] + saver.save(dependency) + sys.modules[dependency] = original(dependency) + try: + real_mod = __import__(modname, {}, {}, modname.split(".")[:-1]) + if modname in ("Queue", "queue") and not hasattr(real_mod, "_threading"): + # tricky hack: Queue's constructor in <2.7 imports + # threading on every instantiation; therefore we wrap + # it so that it always gets the original threading + real_mod.Queue.__init__ = _original_patch_function( + real_mod.Queue.__init__, "threading" + ) + # save a reference to the unpatched module so it doesn't get lost + sys.modules[original_name] = real_mod + finally: + saver.restore() + + return sys.modules[original_name] + + +already_patched = {} + + +def _unmonkey_patch_asyncio(unmonkeypatch_refs_to_this_module): + """ + When using asyncio hub, we want the asyncio modules to use the original, + blocking APIs. So un-monkeypatch references to the given module name, e.g. + "select". + """ + to_unpatch = unmonkeypatch_refs_to_this_module + original_module = original(to_unpatch) + + # Lower down for asyncio modules, we will switch their imported modules to + # original ones instead of the green ones they probably have. This won't + # fix "from socket import whatev" but asyncio doesn't seem to do that in + # ways we care about for Python 3.8 to 3.13, with the one exception of + # get_ident() in some older versions. + if to_unpatch == "_thread": + import asyncio.base_futures + + if hasattr(asyncio.base_futures, "get_ident"): + asyncio.base_futures = original_module.get_ident + + # Asyncio uses these for its blocking thread pool: + if to_unpatch in ("threading", "queue"): + try: + import concurrent.futures.thread + except RuntimeError: + # This happens in weird edge cases where asyncio hub is started at + # shutdown. Not much we can do if this happens. + pass + else: + if to_unpatch == "threading": + concurrent.futures.thread.threading = original_module + if to_unpatch == "queue": + concurrent.futures.thread.queue = original_module + + # Patch asyncio modules: + for module_name in [ + "asyncio.base_events", + "asyncio.base_futures", + "asyncio.base_subprocess", + "asyncio.base_tasks", + "asyncio.constants", + "asyncio.coroutines", + "asyncio.events", + "asyncio.exceptions", + "asyncio.format_helpers", + "asyncio.futures", + "asyncio", + "asyncio.locks", + "asyncio.log", + "asyncio.mixins", + "asyncio.protocols", + "asyncio.queues", + "asyncio.runners", + "asyncio.selector_events", + "asyncio.sslproto", + "asyncio.staggered", + "asyncio.streams", + "asyncio.subprocess", + "asyncio.taskgroups", + "asyncio.tasks", + "asyncio.threads", + "asyncio.timeouts", + "asyncio.transports", + "asyncio.trsock", + "asyncio.unix_events", + ]: + try: + module = importlib.import_module(module_name) + except ImportError: + # The list is from Python 3.13, so some modules may not be present + # in older versions of Python: + continue + if getattr(module, to_unpatch, None) is sys.modules[to_unpatch]: + setattr(module, to_unpatch, original_module) + + +def _unmonkey_patch_asyncio_all(): + """ + Unmonkey-patch all referred-to modules in asyncio. + """ + for module_name, _ in sum([ + _green_os_modules(), + _green_select_modules(), + _green_socket_modules(), + _green_thread_modules(), + _green_time_modules(), + _green_builtins(), + _green_subprocess_modules(), + ], []): + _unmonkey_patch_asyncio(module_name) + original("selectors").select = original("select") + + +def monkey_patch(**on): + """Globally patches certain system modules to be greenthread-friendly. + + The keyword arguments afford some control over which modules are patched. + If no keyword arguments are supplied, all possible modules are patched. + If keywords are set to True, only the specified modules are patched. E.g., + ``monkey_patch(socket=True, select=True)`` patches only the select and + socket modules. Most arguments patch the single module of the same name + (os, time, select). The exceptions are socket, which also patches the ssl + module if present; and thread, which patches thread, threading, and Queue. + + It's safe to call monkey_patch multiple times. + """ + + # Workaround for import cycle observed as following in monotonic + # RuntimeError: no suitable implementation for this system + # see https://github.com/eventlet/eventlet/issues/401#issuecomment-325015989 + # + # Make sure the hub is completely imported before any + # monkey-patching, or we risk recursion if the process of importing + # the hub calls into monkey-patched modules. + eventlet.hubs.get_hub() + + accepted_args = { + "os", + "select", + "socket", + "thread", + "time", + "psycopg", + "MySQLdb", + "builtins", + "subprocess", + } + # To make sure only one of them is passed here + assert not ("__builtin__" in on and "builtins" in on) + try: + b = on.pop("__builtin__") + except KeyError: + pass + else: + on["builtins"] = b + + default_on = on.pop("all", None) + + for k in on.keys(): + if k not in accepted_args: + raise TypeError( + "monkey_patch() got an unexpected " "keyword argument %r" % k + ) + if default_on is None: + default_on = True not in on.values() + for modname in accepted_args: + if modname == "MySQLdb": + # MySQLdb is only on when explicitly patched for the moment + on.setdefault(modname, False) + if modname == "builtins": + on.setdefault(modname, False) + on.setdefault(modname, default_on) + + import threading + + original_rlock_type = type(threading.RLock()) + + modules_to_patch = [] + for name, modules_function in [ + ("os", _green_os_modules), + ("select", _green_select_modules), + ("socket", _green_socket_modules), + ("thread", _green_thread_modules), + ("time", _green_time_modules), + ("MySQLdb", _green_MySQLdb), + ("builtins", _green_builtins), + ("subprocess", _green_subprocess_modules), + ]: + if on[name] and not already_patched.get(name): + modules_to_patch += modules_function() + already_patched[name] = True + + if on["psycopg"] and not already_patched.get("psycopg"): + try: + from eventlet.support import psycopg2_patcher + + psycopg2_patcher.make_psycopg_green() + already_patched["psycopg"] = True + except ImportError: + # note that if we get an importerror from trying to + # monkeypatch psycopg, we will continually retry it + # whenever monkey_patch is called; this should not be a + # performance problem but it allows is_monkey_patched to + # tell us whether or not we succeeded + pass + + _threading = original("threading") + imp.acquire_lock() + try: + for name, mod in modules_to_patch: + orig_mod = sys.modules.get(name) + if orig_mod is None: + orig_mod = __import__(name) + for attr_name in mod.__patched__: + patched_attr = getattr(mod, attr_name, None) + if patched_attr is not None: + setattr(orig_mod, attr_name, patched_attr) + deleted = getattr(mod, "__deleted__", []) + for attr_name in deleted: + if hasattr(orig_mod, attr_name): + delattr(orig_mod, attr_name) + + if name == "threading" and register_at_fork: + # The whole post-fork processing in stdlib threading.py, + # implemented in threading._after_fork(), is based on the + # assumption that threads don't survive fork(). However, green + # threads do survive fork, and that's what threading.py is + # tracking when using eventlet, so there's no need to do any + # post-fork cleanup in this case. + # + # So, we wipe out _after_fork()'s code so it does nothing. We + # can't just override it because it has already been registered + # with os.register_after_fork(). + def noop(): + pass + orig_mod._after_fork.__code__ = noop.__code__ + inject("threading", {})._after_fork.__code__ = noop.__code__ + finally: + imp.release_lock() + + import importlib._bootstrap + + thread = original("_thread") + # importlib must use real thread locks, not eventlet.Semaphore + importlib._bootstrap._thread = thread + + # Issue #185: Since Python 3.3, threading.RLock is implemented in C and + # so call a C function to get the thread identifier, instead of calling + # threading.get_ident(). Force the Python implementation of RLock which + # calls threading.get_ident() and so is compatible with eventlet. + import threading + + threading.RLock = threading._PyRLock + + # Issue #508: Since Python 3.7 queue.SimpleQueue is implemented in C, + # causing a deadlock. Replace the C implementation with the Python one. + import queue + + queue.SimpleQueue = queue._PySimpleQueue + + # Green existing locks _after_ patching modules, since patching modules + # might involve imports that create new locks: + for name, _ in modules_to_patch: + if name == "threading": + _green_existing_locks(original_rlock_type) + + +def is_monkey_patched(module): + """Returns True if the given module is monkeypatched currently, False if + not. *module* can be either the module itself or its name. + + Based entirely off the name of the module, so if you import a + module some other way than with the import keyword (including + import_patched), this might not be correct about that particular + module.""" + return ( + module in already_patched + or getattr(module, "__name__", None) in already_patched + ) + + +def _green_existing_locks(rlock_type): + """Make locks created before monkey-patching safe. + + RLocks rely on a Lock and on Python 2, if an unpatched Lock blocks, it + blocks the native thread. We need to replace these with green Locks. + + This was originally noticed in the stdlib logging module.""" + import gc + import os + import eventlet.green.thread + + # We're monkey-patching so there can't be any greenlets yet, ergo our thread + # ID is the only valid owner possible. + tid = eventlet.green.thread.get_ident() + + # Now, upgrade all instances: + def upgrade(old_lock): + return _convert_py3_rlock(old_lock, tid) + + _upgrade_instances(sys.modules, rlock_type, upgrade) + + # Report if there are RLocks we couldn't upgrade. For cases where we're + # using coverage.py in parent process, and more generally for tests in + # general, this is difficult to ensure, so just don't complain in that case. + if "PYTEST_CURRENT_TEST" in os.environ: + return + # On older Pythons (< 3.10), gc.get_objects() won't return any RLock + # instances, so this warning won't get logged on older Pythons. However, + # it's a useful warning, so we try to do it anyway for the benefit of those + # users on 3.10 or later. + gc.collect() + remaining_rlocks = 0 + for o in gc.get_objects(): + try: + if isinstance(o, rlock_type): + remaining_rlocks += 1 + except ReferenceError as exc: + import logging + import traceback + + logger = logging.Logger("eventlet") + logger.error( + "Not increase rlock count, an exception of type " + + type(exc).__name__ + "occurred with the message '" + + str(exc) + "'. Traceback details: " + + traceback.format_exc() + ) + if remaining_rlocks: + try: + import _frozen_importlib + except ImportError: + pass + else: + for o in gc.get_objects(): + # This can happen in Python 3.12, at least, if monkey patch + # happened as side-effect of importing a module. + try: + if not isinstance(o, rlock_type): + continue + except ReferenceError as exc: + import logging + import traceback + + logger = logging.Logger("eventlet") + logger.error( + "No decrease rlock count, an exception of type " + + type(exc).__name__ + "occurred with the message '" + + str(exc) + "'. Traceback details: " + + traceback.format_exc() + ) + continue # if ReferenceError, skip this object and continue with the next one. + if _frozen_importlib._ModuleLock in map(type, gc.get_referrers(o)): + remaining_rlocks -= 1 + del o + + if remaining_rlocks: + import logging + + logger = logging.Logger("eventlet") + logger.error( + "{} RLock(s) were not greened,".format(remaining_rlocks) + + " to fix this error make sure you run eventlet.monkey_patch() " + + "before importing any other modules." + ) + + +def _upgrade_instances(container, klass, upgrade, visited=None, old_to_new=None): + """ + Starting with a Python object, find all instances of ``klass``, following + references in ``dict`` values, ``list`` items, and attributes. + + Once an object is found, replace all instances with + ``upgrade(found_object)``, again limited to the criteria above. + + In practice this is used only for ``threading.RLock``, so we can assume + instances are hashable. + """ + if visited is None: + visited = {} # map id(obj) to obj + if old_to_new is None: + old_to_new = {} # map old klass instance to upgrade(old) + + # Handle circular references: + visited[id(container)] = container + + def upgrade_or_traverse(obj): + if id(obj) in visited: + return None + if isinstance(obj, klass): + if obj in old_to_new: + return old_to_new[obj] + else: + new = upgrade(obj) + old_to_new[obj] = new + return new + else: + _upgrade_instances(obj, klass, upgrade, visited, old_to_new) + return None + + if isinstance(container, dict): + for k, v in list(container.items()): + new = upgrade_or_traverse(v) + if new is not None: + container[k] = new + if isinstance(container, list): + for i, v in enumerate(container): + new = upgrade_or_traverse(v) + if new is not None: + container[i] = new + try: + container_vars = vars(container) + except TypeError: + pass + else: + # If we get here, we're operating on an object that could + # be doing strange things. If anything bad happens, error and + # warn the eventlet user to monkey_patch earlier. + try: + for k, v in list(container_vars.items()): + new = upgrade_or_traverse(v) + if new is not None: + setattr(container, k, new) + except: + import logging + + logger = logging.Logger("eventlet") + logger.exception( + "An exception was thrown while monkey_patching for eventlet. " + "to fix this error make sure you run eventlet.monkey_patch() " + "before importing any other modules.", + exc_info=True, + ) + + +def _convert_py3_rlock(old, tid): + """ + Convert a normal RLock to one implemented in Python. + + This is necessary to make RLocks work with eventlet, but also introduces + bugs, e.g. https://bugs.python.org/issue13697. So more of a downgrade, + really. + """ + import threading + from eventlet.green.thread import allocate_lock + + new = threading._PyRLock() + if not hasattr(new, "_block") or not hasattr(new, "_owner"): + # These will only fail if Python changes its internal implementation of + # _PyRLock: + raise RuntimeError( + "INTERNAL BUG. Perhaps you are using a major version " + + "of Python that is unsupported by eventlet? Please file a bug " + + "at https://github.com/eventlet/eventlet/issues/new" + ) + new._block = allocate_lock() + acquired = False + while old._is_owned(): + old.release() + new.acquire() + acquired = True + if old._is_owned(): + new.acquire() + acquired = True + if acquired: + new._owner = tid + return new + + +def _green_os_modules(): + from eventlet.green import os + + return [("os", os)] + + +def _green_select_modules(): + from eventlet.green import select + + modules = [("select", select)] + + from eventlet.green import selectors + + modules.append(("selectors", selectors)) + + return modules + + +def _green_socket_modules(): + from eventlet.green import socket + + try: + from eventlet.green import ssl + + return [("socket", socket), ("ssl", ssl)] + except ImportError: + return [("socket", socket)] + + +def _green_subprocess_modules(): + from eventlet.green import subprocess + + return [("subprocess", subprocess)] + + +def _green_thread_modules(): + from eventlet.green import Queue + from eventlet.green import thread + from eventlet.green import threading + + return [("queue", Queue), ("_thread", thread), ("threading", threading)] + + +def _green_time_modules(): + from eventlet.green import time + + return [("time", time)] + + +def _green_MySQLdb(): + try: + from eventlet.green import MySQLdb + + return [("MySQLdb", MySQLdb)] + except ImportError: + return [] + + +def _green_builtins(): + try: + from eventlet.green import builtin + + return [("builtins", builtin)] + except ImportError: + return [] + + +def slurp_properties(source, destination, ignore=[], srckeys=None): + """Copy properties from *source* (assumed to be a module) to + *destination* (assumed to be a dict). + + *ignore* lists properties that should not be thusly copied. + *srckeys* is a list of keys to copy, if the source's __all__ is + untrustworthy. + """ + if srckeys is None: + srckeys = source.__all__ + destination.update( + { + name: getattr(source, name) + for name in srckeys + if not (name.startswith("__") or name in ignore) + } + ) + + +if __name__ == "__main__": + sys.argv.pop(0) + monkey_patch() + with open(sys.argv[0]) as f: + code = compile(f.read(), sys.argv[0], "exec") + exec(code) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/pools.py b/netdeploy/lib/python3.11/site-packages/eventlet/pools.py new file mode 100644 index 0000000..a65f174 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/pools.py @@ -0,0 +1,184 @@ +import collections +from contextlib import contextmanager + +from eventlet import queue + + +__all__ = ['Pool', 'TokenPool'] + + +class Pool: + """ + Pool class implements resource limitation and construction. + + There are two ways of using Pool: passing a `create` argument or + subclassing. In either case you must provide a way to create + the resource. + + When using `create` argument, pass a function with no arguments:: + + http_pool = pools.Pool(create=httplib2.Http) + + If you need to pass arguments, build a nullary function with either + `lambda` expression:: + + http_pool = pools.Pool(create=lambda: httplib2.Http(timeout=90)) + + or :func:`functools.partial`:: + + from functools import partial + http_pool = pools.Pool(create=partial(httplib2.Http, timeout=90)) + + When subclassing, define only the :meth:`create` method + to implement the desired resource:: + + class MyPool(pools.Pool): + def create(self): + return MyObject() + + If using 2.5 or greater, the :meth:`item` method acts as a context manager; + that's the best way to use it:: + + with mypool.item() as thing: + thing.dostuff() + + The maximum size of the pool can be modified at runtime via + the :meth:`resize` method. + + Specifying a non-zero *min-size* argument pre-populates the pool with + *min_size* items. *max-size* sets a hard limit to the size of the pool -- + it cannot contain any more items than *max_size*, and if there are already + *max_size* items 'checked out' of the pool, the pool will cause any + greenthread calling :meth:`get` to cooperatively yield until an item + is :meth:`put` in. + """ + + def __init__(self, min_size=0, max_size=4, order_as_stack=False, create=None): + """*order_as_stack* governs the ordering of the items in the free pool. + If ``False`` (the default), the free items collection (of items that + were created and were put back in the pool) acts as a round-robin, + giving each item approximately equal utilization. If ``True``, the + free pool acts as a FILO stack, which preferentially re-uses items that + have most recently been used. + """ + self.min_size = min_size + self.max_size = max_size + self.order_as_stack = order_as_stack + self.current_size = 0 + self.channel = queue.LightQueue(0) + self.free_items = collections.deque() + if create is not None: + self.create = create + + for x in range(min_size): + self.current_size += 1 + self.free_items.append(self.create()) + + def get(self): + """Return an item from the pool, when one is available. This may + cause the calling greenthread to block. + """ + if self.free_items: + return self.free_items.popleft() + self.current_size += 1 + if self.current_size <= self.max_size: + try: + created = self.create() + except: + self.current_size -= 1 + raise + return created + self.current_size -= 1 # did not create + return self.channel.get() + + @contextmanager + def item(self): + """ Get an object out of the pool, for use with with statement. + + >>> from eventlet import pools + >>> pool = pools.TokenPool(max_size=4) + >>> with pool.item() as obj: + ... print("got token") + ... + got token + >>> pool.free() + 4 + """ + obj = self.get() + try: + yield obj + finally: + self.put(obj) + + def put(self, item): + """Put an item back into the pool, when done. This may + cause the putting greenthread to block. + """ + if self.current_size > self.max_size: + self.current_size -= 1 + return + + if self.waiting(): + try: + self.channel.put(item, block=False) + return + except queue.Full: + pass + + if self.order_as_stack: + self.free_items.appendleft(item) + else: + self.free_items.append(item) + + def resize(self, new_size): + """Resize the pool to *new_size*. + + Adjusting this number does not affect existing items checked out of + the pool, nor on any greenthreads who are waiting for an item to free + up. Some indeterminate number of :meth:`get`/:meth:`put` + cycles will be necessary before the new maximum size truly matches + the actual operation of the pool. + """ + self.max_size = new_size + + def free(self): + """Return the number of free items in the pool. This corresponds + to the number of :meth:`get` calls needed to empty the pool. + """ + return len(self.free_items) + self.max_size - self.current_size + + def waiting(self): + """Return the number of routines waiting for a pool item. + """ + return max(0, self.channel.getting() - self.channel.putting()) + + def create(self): + """Generate a new pool item. In order for the pool to + function, either this method must be overriden in a subclass + or the pool must be constructed with the `create` argument. + It accepts no arguments and returns a single instance of + whatever thing the pool is supposed to contain. + + In general, :meth:`create` is called whenever the pool exceeds its + previous high-water mark of concurrently-checked-out-items. In other + words, in a new pool with *min_size* of 0, the very first call + to :meth:`get` will result in a call to :meth:`create`. If the first + caller calls :meth:`put` before some other caller calls :meth:`get`, + then the first item will be returned, and :meth:`create` will not be + called a second time. + """ + raise NotImplementedError("Implement in subclass") + + +class Token: + pass + + +class TokenPool(Pool): + """A pool which gives out tokens (opaque unique objects), which indicate + that the coroutine which holds the token has a right to consume some + limited resource. + """ + + def create(self): + return Token() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/queue.py b/netdeploy/lib/python3.11/site-packages/eventlet/queue.py new file mode 100644 index 0000000..d3bd4dc --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/queue.py @@ -0,0 +1,496 @@ +# Copyright (c) 2009 Denis Bilenko, denis.bilenko at gmail com +# Copyright (c) 2010 Eventlet Contributors (see AUTHORS) +# and licensed under the MIT license: +# +# 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. + +"""Synchronized queues. + +The :mod:`eventlet.queue` module implements multi-producer, multi-consumer +queues that work across greenlets, with the API similar to the classes found in +the standard :mod:`Queue` and :class:`multiprocessing ` +modules. + +A major difference is that queues in this module operate as channels when +initialized with *maxsize* of zero. In such case, both :meth:`Queue.empty` +and :meth:`Queue.full` return ``True`` and :meth:`Queue.put` always blocks until +a call to :meth:`Queue.get` retrieves the item. + +An interesting difference, made possible because of greenthreads, is +that :meth:`Queue.qsize`, :meth:`Queue.empty`, and :meth:`Queue.full` *can* be +used as indicators of whether the subsequent :meth:`Queue.get` +or :meth:`Queue.put` will not block. The new methods :meth:`Queue.getting` +and :meth:`Queue.putting` report on the number of greenthreads blocking +in :meth:`put ` or :meth:`get ` respectively. +""" + +import collections +import heapq +import sys +import traceback +import types + +from eventlet.event import Event +from eventlet.greenthread import getcurrent +from eventlet.hubs import get_hub +import queue as Stdlib_Queue +from eventlet.timeout import Timeout + + +__all__ = ['Queue', 'PriorityQueue', 'LifoQueue', 'LightQueue', 'Full', 'Empty'] + +_NONE = object() +Full = Stdlib_Queue.Full +Empty = Stdlib_Queue.Empty + + +class Waiter: + """A low level synchronization class. + + Wrapper around greenlet's ``switch()`` and ``throw()`` calls that makes them safe: + + * switching will occur only if the waiting greenlet is executing :meth:`wait` + method currently. Otherwise, :meth:`switch` and :meth:`throw` are no-ops. + * any error raised in the greenlet is handled inside :meth:`switch` and :meth:`throw` + + The :meth:`switch` and :meth:`throw` methods must only be called from the :class:`Hub` greenlet. + The :meth:`wait` method must be called from a greenlet other than :class:`Hub`. + """ + __slots__ = ['greenlet'] + + def __init__(self): + self.greenlet = None + + def __repr__(self): + if self.waiting: + waiting = ' waiting' + else: + waiting = '' + return '<%s at %s%s greenlet=%r>' % ( + type(self).__name__, hex(id(self)), waiting, self.greenlet, + ) + + def __str__(self): + """ + >>> print(Waiter()) + + """ + if self.waiting: + waiting = ' waiting' + else: + waiting = '' + return '<%s%s greenlet=%s>' % (type(self).__name__, waiting, self.greenlet) + + def __nonzero__(self): + return self.greenlet is not None + + __bool__ = __nonzero__ + + @property + def waiting(self): + return self.greenlet is not None + + def switch(self, value=None): + """Wake up the greenlet that is calling wait() currently (if there is one). + Can only be called from Hub's greenlet. + """ + assert getcurrent() is get_hub( + ).greenlet, "Can only use Waiter.switch method from the mainloop" + if self.greenlet is not None: + try: + self.greenlet.switch(value) + except Exception: + traceback.print_exc() + + def throw(self, *throw_args): + """Make greenlet calling wait() wake up (if there is a wait()). + Can only be called from Hub's greenlet. + """ + assert getcurrent() is get_hub( + ).greenlet, "Can only use Waiter.switch method from the mainloop" + if self.greenlet is not None: + try: + self.greenlet.throw(*throw_args) + except Exception: + traceback.print_exc() + + # XXX should be renamed to get() ? and the whole class is called Receiver? + def wait(self): + """Wait until switch() or throw() is called. + """ + assert self.greenlet is None, 'This Waiter is already used by %r' % (self.greenlet, ) + self.greenlet = getcurrent() + try: + return get_hub().switch() + finally: + self.greenlet = None + + +class LightQueue: + """ + This is a variant of Queue that behaves mostly like the standard + :class:`Stdlib_Queue`. It differs by not supporting the + :meth:`task_done ` or + :meth:`join ` methods, and is a little faster for + not having that overhead. + """ + + def __init__(self, maxsize=None): + if maxsize is None or maxsize < 0: # None is not comparable in 3.x + self.maxsize = None + else: + self.maxsize = maxsize + self.getters = set() + self.putters = set() + self._event_unlock = None + self._init(maxsize) + + # QQQ make maxsize into a property with setter that schedules unlock if necessary + + def _init(self, maxsize): + self.queue = collections.deque() + + def _get(self): + return self.queue.popleft() + + def _put(self, item): + self.queue.append(item) + + def __repr__(self): + return '<%s at %s %s>' % (type(self).__name__, hex(id(self)), self._format()) + + def __str__(self): + return '<%s %s>' % (type(self).__name__, self._format()) + + def _format(self): + result = 'maxsize=%r' % (self.maxsize, ) + if getattr(self, 'queue', None): + result += ' queue=%r' % self.queue + if self.getters: + result += ' getters[%s]' % len(self.getters) + if self.putters: + result += ' putters[%s]' % len(self.putters) + if self._event_unlock is not None: + result += ' unlocking' + return result + + def qsize(self): + """Return the size of the queue.""" + return len(self.queue) + + def resize(self, size): + """Resizes the queue's maximum size. + + If the size is increased, and there are putters waiting, they may be woken up.""" + # None is not comparable in 3.x + if self.maxsize is not None and (size is None or size > self.maxsize): + # Maybe wake some stuff up + self._schedule_unlock() + self.maxsize = size + + def putting(self): + """Returns the number of greenthreads that are blocked waiting to put + items into the queue.""" + return len(self.putters) + + def getting(self): + """Returns the number of greenthreads that are blocked waiting on an + empty queue.""" + return len(self.getters) + + def empty(self): + """Return ``True`` if the queue is empty, ``False`` otherwise.""" + return not self.qsize() + + def full(self): + """Return ``True`` if the queue is full, ``False`` otherwise. + + ``Queue(None)`` is never full. + """ + # None is not comparable in 3.x + return self.maxsize is not None and self.qsize() >= self.maxsize + + def put(self, item, block=True, timeout=None): + """Put an item into the queue. + + If optional arg *block* is true and *timeout* is ``None`` (the default), + block if necessary until a free slot is available. If *timeout* is + a positive number, it blocks at most *timeout* seconds and raises + the :class:`Full` exception if no free slot was available within that time. + Otherwise (*block* is false), put an item on the queue if a free slot + is immediately available, else raise the :class:`Full` exception (*timeout* + is ignored in that case). + """ + if self.maxsize is None or self.qsize() < self.maxsize: + # there's a free slot, put an item right away + self._put(item) + if self.getters: + self._schedule_unlock() + elif not block and get_hub().greenlet is getcurrent(): + # we're in the mainloop, so we cannot wait; we can switch() to other greenlets though + # find a getter and deliver an item to it + while self.getters: + getter = self.getters.pop() + if getter: + self._put(item) + item = self._get() + getter.switch(item) + return + raise Full + elif block: + waiter = ItemWaiter(item, block) + self.putters.add(waiter) + timeout = Timeout(timeout, Full) + try: + if self.getters: + self._schedule_unlock() + result = waiter.wait() + assert result is waiter, "Invalid switch into Queue.put: %r" % (result, ) + if waiter.item is not _NONE: + self._put(item) + finally: + timeout.cancel() + self.putters.discard(waiter) + elif self.getters: + waiter = ItemWaiter(item, block) + self.putters.add(waiter) + self._schedule_unlock() + result = waiter.wait() + assert result is waiter, "Invalid switch into Queue.put: %r" % (result, ) + if waiter.item is not _NONE: + raise Full + else: + raise Full + + def put_nowait(self, item): + """Put an item into the queue without blocking. + + Only enqueue the item if a free slot is immediately available. + Otherwise raise the :class:`Full` exception. + """ + self.put(item, False) + + def get(self, block=True, timeout=None): + """Remove and return an item from the queue. + + If optional args *block* is true and *timeout* is ``None`` (the default), + block if necessary until an item is available. If *timeout* is a positive number, + it blocks at most *timeout* seconds and raises the :class:`Empty` exception + if no item was available within that time. Otherwise (*block* is false), return + an item if one is immediately available, else raise the :class:`Empty` exception + (*timeout* is ignored in that case). + """ + if self.qsize(): + if self.putters: + self._schedule_unlock() + return self._get() + elif not block and get_hub().greenlet is getcurrent(): + # special case to make get_nowait() runnable in the mainloop greenlet + # there are no items in the queue; try to fix the situation by unlocking putters + while self.putters: + putter = self.putters.pop() + if putter: + putter.switch(putter) + if self.qsize(): + return self._get() + raise Empty + elif block: + waiter = Waiter() + timeout = Timeout(timeout, Empty) + try: + self.getters.add(waiter) + if self.putters: + self._schedule_unlock() + try: + return waiter.wait() + except: + self._schedule_unlock() + raise + finally: + self.getters.discard(waiter) + timeout.cancel() + else: + raise Empty + + def get_nowait(self): + """Remove and return an item from the queue without blocking. + + Only get an item if one is immediately available. Otherwise + raise the :class:`Empty` exception. + """ + return self.get(False) + + def _unlock(self): + try: + while True: + if self.qsize() and self.getters: + getter = self.getters.pop() + if getter: + try: + item = self._get() + except: + getter.throw(*sys.exc_info()) + else: + getter.switch(item) + elif self.putters and self.getters: + putter = self.putters.pop() + if putter: + getter = self.getters.pop() + if getter: + item = putter.item + # this makes greenlet calling put() not to call _put() again + putter.item = _NONE + self._put(item) + item = self._get() + getter.switch(item) + putter.switch(putter) + else: + self.putters.add(putter) + elif self.putters and (self.getters or + self.maxsize is None or + self.qsize() < self.maxsize): + putter = self.putters.pop() + putter.switch(putter) + elif self.putters and not self.getters: + full = [p for p in self.putters if not p.block] + if not full: + break + for putter in full: + self.putters.discard(putter) + get_hub().schedule_call_global( + 0, putter.greenlet.throw, Full) + else: + break + finally: + self._event_unlock = None # QQQ maybe it's possible to obtain this info from libevent? + # i.e. whether this event is pending _OR_ currently executing + # testcase: 2 greenlets: while True: q.put(q.get()) - nothing else has a change to execute + # to avoid this, schedule unlock with timer(0, ...) once in a while + + def _schedule_unlock(self): + if self._event_unlock is None: + self._event_unlock = get_hub().schedule_call_global(0, self._unlock) + + # TODO(stephenfin): Remove conditional when we bump the minimum Python + # version + if sys.version_info >= (3, 9): + __class_getitem__ = classmethod(types.GenericAlias) + + +class ItemWaiter(Waiter): + __slots__ = ['item', 'block'] + + def __init__(self, item, block): + Waiter.__init__(self) + self.item = item + self.block = block + + +class Queue(LightQueue): + '''Create a queue object with a given maximum size. + + If *maxsize* is less than zero or ``None``, the queue size is infinite. + + ``Queue(0)`` is a channel, that is, its :meth:`put` method always blocks + until the item is delivered. (This is unlike the standard + :class:`Stdlib_Queue`, where 0 means infinite size). + + In all other respects, this Queue class resembles the standard library, + :class:`Stdlib_Queue`. + ''' + + def __init__(self, maxsize=None): + LightQueue.__init__(self, maxsize) + self.unfinished_tasks = 0 + self._cond = Event() + + def _format(self): + result = LightQueue._format(self) + if self.unfinished_tasks: + result += ' tasks=%s _cond=%s' % (self.unfinished_tasks, self._cond) + return result + + def _put(self, item): + LightQueue._put(self, item) + self._put_bookkeeping() + + def _put_bookkeeping(self): + self.unfinished_tasks += 1 + if self._cond.ready(): + self._cond.reset() + + def task_done(self): + '''Indicate that a formerly enqueued task is complete. Used by queue consumer threads. + For each :meth:`get ` used to fetch a task, a subsequent call to + :meth:`task_done` tells the queue that the processing on the task is complete. + + If a :meth:`join` is currently blocking, it will resume when all items have been processed + (meaning that a :meth:`task_done` call was received for every item that had been + :meth:`put ` into the queue). + + Raises a :exc:`ValueError` if called more times than there were items placed in the queue. + ''' + + if self.unfinished_tasks <= 0: + raise ValueError('task_done() called too many times') + self.unfinished_tasks -= 1 + if self.unfinished_tasks == 0: + self._cond.send(None) + + def join(self): + '''Block until all items in the queue have been gotten and processed. + + The count of unfinished tasks goes up whenever an item is added to the queue. + The count goes down whenever a consumer thread calls :meth:`task_done` to indicate + that the item was retrieved and all work on it is complete. When the count of + unfinished tasks drops to zero, :meth:`join` unblocks. + ''' + if self.unfinished_tasks > 0: + self._cond.wait() + + +class PriorityQueue(Queue): + '''A subclass of :class:`Queue` that retrieves entries in priority order (lowest first). + + Entries are typically tuples of the form: ``(priority number, data)``. + ''' + + def _init(self, maxsize): + self.queue = [] + + def _put(self, item, heappush=heapq.heappush): + heappush(self.queue, item) + self._put_bookkeeping() + + def _get(self, heappop=heapq.heappop): + return heappop(self.queue) + + +class LifoQueue(Queue): + '''A subclass of :class:`Queue` that retrieves most recently added entries first.''' + + def _init(self, maxsize): + self.queue = [] + + def _put(self, item): + self.queue.append(item) + self._put_bookkeeping() + + def _get(self): + return self.queue.pop() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/semaphore.py b/netdeploy/lib/python3.11/site-packages/eventlet/semaphore.py new file mode 100644 index 0000000..218d01a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/semaphore.py @@ -0,0 +1,315 @@ +import collections + +import eventlet +from eventlet import hubs + + +class Semaphore: + + """An unbounded semaphore. + Optionally initialize with a resource *count*, then :meth:`acquire` and + :meth:`release` resources as needed. Attempting to :meth:`acquire` when + *count* is zero suspends the calling greenthread until *count* becomes + nonzero again. + + This is API-compatible with :class:`threading.Semaphore`. + + It is a context manager, and thus can be used in a with block:: + + sem = Semaphore(2) + with sem: + do_some_stuff() + + If not specified, *value* defaults to 1. + + It is possible to limit acquire time:: + + sem = Semaphore() + ok = sem.acquire(timeout=0.1) + # True if acquired, False if timed out. + + """ + + def __init__(self, value=1): + try: + value = int(value) + except ValueError as e: + msg = 'Semaphore() expect value :: int, actual: {} {}'.format(type(value), str(e)) + raise TypeError(msg) + if value < 0: + msg = 'Semaphore() expect value >= 0, actual: {}'.format(repr(value)) + raise ValueError(msg) + self.counter = value + self._waiters = collections.deque() + + def __repr__(self): + params = (self.__class__.__name__, hex(id(self)), + self.counter, len(self._waiters)) + return '<%s at %s c=%s _w[%s]>' % params + + def __str__(self): + params = (self.__class__.__name__, self.counter, len(self._waiters)) + return '<%s c=%s _w[%s]>' % params + + def locked(self): + """Returns true if a call to acquire would block. + """ + return self.counter <= 0 + + def bounded(self): + """Returns False; for consistency with + :class:`~eventlet.semaphore.CappedSemaphore`. + """ + return False + + def acquire(self, blocking=True, timeout=None): + """Acquire a semaphore. + + When invoked without arguments: if the internal counter is larger than + zero on entry, decrement it by one and return immediately. If it is zero + on entry, block, waiting until some other thread has called release() to + make it larger than zero. This is done with proper interlocking so that + if multiple acquire() calls are blocked, release() will wake exactly one + of them up. The implementation may pick one at random, so the order in + which blocked threads are awakened should not be relied on. There is no + return value in this case. + + When invoked with blocking set to true, do the same thing as when called + without arguments, and return true. + + When invoked with blocking set to false, do not block. If a call without + an argument would block, return false immediately; otherwise, do the + same thing as when called without arguments, and return true. + + Timeout value must be strictly positive. + """ + if timeout == -1: + timeout = None + if timeout is not None and timeout < 0: + raise ValueError("timeout value must be strictly positive") + if not blocking: + if timeout is not None: + raise ValueError("can't specify timeout for non-blocking acquire") + timeout = 0 + if not blocking and self.locked(): + return False + + current_thread = eventlet.getcurrent() + + if self.counter <= 0 or self._waiters: + if current_thread not in self._waiters: + self._waiters.append(current_thread) + try: + if timeout is not None: + ok = False + with eventlet.Timeout(timeout, False): + while self.counter <= 0: + hubs.get_hub().switch() + ok = True + if not ok: + return False + else: + # If someone else is already in this wait loop, give them + # a chance to get out. + while True: + hubs.get_hub().switch() + if self.counter > 0: + break + finally: + try: + self._waiters.remove(current_thread) + except ValueError: + # Fine if its already been dropped. + pass + + self.counter -= 1 + return True + + def __enter__(self): + self.acquire() + + def release(self, blocking=True): + """Release a semaphore, incrementing the internal counter by one. When + it was zero on entry and another thread is waiting for it to become + larger than zero again, wake up that thread. + + The *blocking* argument is for consistency with CappedSemaphore and is + ignored + """ + self.counter += 1 + if self._waiters: + hubs.get_hub().schedule_call_global(0, self._do_acquire) + return True + + def _do_acquire(self): + if self._waiters and self.counter > 0: + waiter = self._waiters.popleft() + waiter.switch() + + def __exit__(self, typ, val, tb): + self.release() + + @property + def balance(self): + """An integer value that represents how many new calls to + :meth:`acquire` or :meth:`release` would be needed to get the counter to + 0. If it is positive, then its value is the number of acquires that can + happen before the next acquire would block. If it is negative, it is + the negative of the number of releases that would be required in order + to make the counter 0 again (one more release would push the counter to + 1 and unblock acquirers). It takes into account how many greenthreads + are currently blocking in :meth:`acquire`. + """ + # positive means there are free items + # zero means there are no free items but nobody has requested one + # negative means there are requests for items, but no items + return self.counter - len(self._waiters) + + +class BoundedSemaphore(Semaphore): + + """A bounded semaphore checks to make sure its current value doesn't exceed + its initial value. If it does, ValueError is raised. In most situations + semaphores are used to guard resources with limited capacity. If the + semaphore is released too many times it's a sign of a bug. If not given, + *value* defaults to 1. + """ + + def __init__(self, value=1): + super().__init__(value) + self.original_counter = value + + def release(self, blocking=True): + """Release a semaphore, incrementing the internal counter by one. If + the counter would exceed the initial value, raises ValueError. When + it was zero on entry and another thread is waiting for it to become + larger than zero again, wake up that thread. + + The *blocking* argument is for consistency with :class:`CappedSemaphore` + and is ignored + """ + if self.counter >= self.original_counter: + raise ValueError("Semaphore released too many times") + return super().release(blocking) + + +class CappedSemaphore: + + """A blockingly bounded semaphore. + + Optionally initialize with a resource *count*, then :meth:`acquire` and + :meth:`release` resources as needed. Attempting to :meth:`acquire` when + *count* is zero suspends the calling greenthread until count becomes nonzero + again. Attempting to :meth:`release` after *count* has reached *limit* + suspends the calling greenthread until *count* becomes less than *limit* + again. + + This has the same API as :class:`threading.Semaphore`, though its + semantics and behavior differ subtly due to the upper limit on calls + to :meth:`release`. It is **not** compatible with + :class:`threading.BoundedSemaphore` because it blocks when reaching *limit* + instead of raising a ValueError. + + It is a context manager, and thus can be used in a with block:: + + sem = CappedSemaphore(2) + with sem: + do_some_stuff() + """ + + def __init__(self, count, limit): + if count < 0: + raise ValueError("CappedSemaphore must be initialized with a " + "positive number, got %s" % count) + if count > limit: + # accidentally, this also catches the case when limit is None + raise ValueError("'count' cannot be more than 'limit'") + self.lower_bound = Semaphore(count) + self.upper_bound = Semaphore(limit - count) + + def __repr__(self): + params = (self.__class__.__name__, hex(id(self)), + self.balance, self.lower_bound, self.upper_bound) + return '<%s at %s b=%s l=%s u=%s>' % params + + def __str__(self): + params = (self.__class__.__name__, self.balance, + self.lower_bound, self.upper_bound) + return '<%s b=%s l=%s u=%s>' % params + + def locked(self): + """Returns true if a call to acquire would block. + """ + return self.lower_bound.locked() + + def bounded(self): + """Returns true if a call to release would block. + """ + return self.upper_bound.locked() + + def acquire(self, blocking=True): + """Acquire a semaphore. + + When invoked without arguments: if the internal counter is larger than + zero on entry, decrement it by one and return immediately. If it is zero + on entry, block, waiting until some other thread has called release() to + make it larger than zero. This is done with proper interlocking so that + if multiple acquire() calls are blocked, release() will wake exactly one + of them up. The implementation may pick one at random, so the order in + which blocked threads are awakened should not be relied on. There is no + return value in this case. + + When invoked with blocking set to true, do the same thing as when called + without arguments, and return true. + + When invoked with blocking set to false, do not block. If a call without + an argument would block, return false immediately; otherwise, do the + same thing as when called without arguments, and return true. + """ + if not blocking and self.locked(): + return False + self.upper_bound.release() + try: + return self.lower_bound.acquire() + except: + self.upper_bound.counter -= 1 + # using counter directly means that it can be less than zero. + # however I certainly don't need to wait here and I don't seem to have + # a need to care about such inconsistency + raise + + def __enter__(self): + self.acquire() + + def release(self, blocking=True): + """Release a semaphore. In this class, this behaves very much like + an :meth:`acquire` but in the opposite direction. + + Imagine the docs of :meth:`acquire` here, but with every direction + reversed. When calling this method, it will block if the internal + counter is greater than or equal to *limit*. + """ + if not blocking and self.bounded(): + return False + self.lower_bound.release() + try: + return self.upper_bound.acquire() + except: + self.lower_bound.counter -= 1 + raise + + def __exit__(self, typ, val, tb): + self.release() + + @property + def balance(self): + """An integer value that represents how many new calls to + :meth:`acquire` or :meth:`release` would be needed to get the counter to + 0. If it is positive, then its value is the number of acquires that can + happen before the next acquire would block. If it is negative, it is + the negative of the number of releases that would be required in order + to make the counter 0 again (one more release would push the counter to + 1 and unblock acquirers). It takes into account how many greenthreads + are currently blocking in :meth:`acquire` and :meth:`release`. + """ + return self.lower_bound.balance - self.upper_bound.balance diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/support/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/support/__init__.py new file mode 100644 index 0000000..b1c1607 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/support/__init__.py @@ -0,0 +1,69 @@ +import inspect +import functools +import sys +import warnings + +from eventlet.support import greenlets + + +_MISSING = object() + + +def get_errno(exc): + """ Get the error code out of socket.error objects. + socket.error in <2.5 does not have errno attribute + socket.error in 3.x does not allow indexing access + e.args[0] works for all. + There are cases when args[0] is not errno. + i.e. http://bugs.python.org/issue6471 + Maybe there are cases when errno is set, but it is not the first argument? + """ + + try: + if exc.errno is not None: + return exc.errno + except AttributeError: + pass + try: + return exc.args[0] + except IndexError: + return None + + +if sys.version_info[0] < 3: + def bytes_to_str(b, encoding='ascii'): + return b +else: + def bytes_to_str(b, encoding='ascii'): + return b.decode(encoding) + +PY33 = sys.version_info[:2] == (3, 3) + + +def wrap_deprecated(old, new): + def _resolve(s): + return 'eventlet.'+s if '.' not in s else s + msg = '''\ +{old} is deprecated and will be removed in next version. Use {new} instead. +Autoupgrade: fgrep -rl '{old}' . |xargs -t sed --in-place='' -e 's/{old}/{new}/' +'''.format(old=_resolve(old), new=_resolve(new)) + + def wrapper(base): + klass = None + if inspect.isclass(base): + class klass(base): + pass + klass.__name__ = base.__name__ + klass.__module__ = base.__module__ + + @functools.wraps(base) + def wrapped(*a, **kw): + warnings.warn(msg, DeprecationWarning, stacklevel=5) + return base(*a, **kw) + + if klass is not None: + klass.__init__ = wrapped + return klass + + return wrapped + return wrapper diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/support/greendns.py b/netdeploy/lib/python3.11/site-packages/eventlet/support/greendns.py new file mode 100644 index 0000000..365664f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/support/greendns.py @@ -0,0 +1,959 @@ +'''greendns - non-blocking DNS support for Eventlet +''' + +# Portions of this code taken from the gogreen project: +# http://github.com/slideinc/gogreen +# +# Copyright (c) 2005-2010 Slide, Inc. +# 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 the author nor the names of other +# 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 +# OWNER 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. +import re +import struct +import sys + +import eventlet +from eventlet import patcher +from eventlet.green import _socket_nodns +from eventlet.green import os +from eventlet.green import time +from eventlet.green import select +from eventlet.green import ssl + + +def import_patched(module_name): + # Import cycle note: it's crucial to use _socket_nodns here because + # regular evenlet.green.socket imports *this* module and if we imported + # it back we'd end with an import cycle (socket -> greendns -> socket). + # We break this import cycle by providing a restricted socket module. + modules = { + 'select': select, + 'time': time, + 'os': os, + 'socket': _socket_nodns, + 'ssl': ssl, + } + return patcher.import_patched(module_name, **modules) + + +dns = import_patched('dns') + +# Handle rdtypes separately; we need fully it available as we patch the rest +dns.rdtypes = import_patched('dns.rdtypes') +dns.rdtypes.__all__.extend(['dnskeybase', 'dsbase', 'txtbase']) +for pkg in dns.rdtypes.__all__: + setattr(dns.rdtypes, pkg, import_patched('dns.rdtypes.' + pkg)) +for pkg in dns.rdtypes.IN.__all__: + setattr(dns.rdtypes.IN, pkg, import_patched('dns.rdtypes.IN.' + pkg)) +for pkg in dns.rdtypes.ANY.__all__: + setattr(dns.rdtypes.ANY, pkg, import_patched('dns.rdtypes.ANY.' + pkg)) + +for pkg in dns.__all__: + if pkg == 'rdtypes': + continue + setattr(dns, pkg, import_patched('dns.' + pkg)) +del import_patched + + +socket = _socket_nodns + +DNS_QUERY_TIMEOUT = 10.0 +HOSTS_TTL = 10.0 + +# NOTE(victor): do not use EAI_*_ERROR instances for raising errors in python3, which will cause a memory leak. +EAI_EAGAIN_ERROR = socket.gaierror(socket.EAI_AGAIN, 'Lookup timed out') +EAI_NONAME_ERROR = socket.gaierror(socket.EAI_NONAME, 'Name or service not known') +# EAI_NODATA was removed from RFC3493, it's now replaced with EAI_NONAME +# socket.EAI_NODATA is not defined on FreeBSD, probably on some other platforms too. +# https://lists.freebsd.org/pipermail/freebsd-ports/2003-October/005757.html +EAI_NODATA_ERROR = EAI_NONAME_ERROR +if (os.environ.get('EVENTLET_DEPRECATED_EAI_NODATA', '').lower() in ('1', 'y', 'yes') + and hasattr(socket, 'EAI_NODATA')): + EAI_NODATA_ERROR = socket.gaierror(socket.EAI_NODATA, 'No address associated with hostname') + + +def _raise_new_error(error_instance): + raise error_instance.__class__(*error_instance.args) + + +def is_ipv4_addr(host): + """Return True if host is a valid IPv4 address""" + if not isinstance(host, str): + return False + try: + dns.ipv4.inet_aton(host) + except dns.exception.SyntaxError: + return False + else: + return True + + +def is_ipv6_addr(host): + """Return True if host is a valid IPv6 address""" + if not isinstance(host, str): + return False + host = host.split('%', 1)[0] + try: + dns.ipv6.inet_aton(host) + except dns.exception.SyntaxError: + return False + else: + return True + + +def is_ip_addr(host): + """Return True if host is a valid IPv4 or IPv6 address""" + return is_ipv4_addr(host) or is_ipv6_addr(host) + + +# NOTE(ralonsoh): in dnspython v2.0.0, "_compute_expiration" was replaced +# by "_compute_times". +if hasattr(dns.query, '_compute_expiration'): + def compute_expiration(query, timeout): + return query._compute_expiration(timeout) +else: + def compute_expiration(query, timeout): + return query._compute_times(timeout)[1] + + +class HostsAnswer(dns.resolver.Answer): + """Answer class for HostsResolver object""" + + def __init__(self, qname, rdtype, rdclass, rrset, raise_on_no_answer=True): + """Create a new answer + + :qname: A dns.name.Name instance of the query name + :rdtype: The rdatatype of the query + :rdclass: The rdataclass of the query + :rrset: The dns.rrset.RRset with the response, must have ttl attribute + :raise_on_no_answer: Whether to raise dns.resolver.NoAnswer if no + answer. + """ + self.response = None + self.qname = qname + self.rdtype = rdtype + self.rdclass = rdclass + self.canonical_name = qname + if not rrset and raise_on_no_answer: + raise dns.resolver.NoAnswer() + self.rrset = rrset + self.expiration = (time.time() + + rrset.ttl if hasattr(rrset, 'ttl') else 0) + + +class HostsResolver: + """Class to parse the hosts file + + Attributes + ---------- + + :fname: The filename of the hosts file in use. + :interval: The time between checking for hosts file modification + """ + + LINES_RE = re.compile(r""" + \s* # Leading space + ([^\r\n#]*?) # The actual match, non-greedy so as not to include trailing space + \s* # Trailing space + (?:[#][^\r\n]+)? # Comments + (?:$|[\r\n]+) # EOF or newline + """, re.VERBOSE) + + def __init__(self, fname=None, interval=HOSTS_TTL): + self._v4 = {} # name -> ipv4 + self._v6 = {} # name -> ipv6 + self._aliases = {} # name -> canonical_name + self.interval = interval + self.fname = fname + if fname is None: + if os.name == 'posix': + self.fname = '/etc/hosts' + elif os.name == 'nt': + self.fname = os.path.expandvars( + r'%SystemRoot%\system32\drivers\etc\hosts') + self._last_load = 0 + if self.fname: + self._load() + + def _readlines(self): + """Read the contents of the hosts file + + Return list of lines, comment lines and empty lines are + excluded. + + Note that this performs disk I/O so can be blocking. + """ + try: + with open(self.fname, 'rb') as fp: + fdata = fp.read() + except OSError: + return [] + + udata = fdata.decode(errors='ignore') + + return filter(None, self.LINES_RE.findall(udata)) + + def _load(self): + """Load hosts file + + This will unconditionally (re)load the data from the hosts + file. + """ + lines = self._readlines() + self._v4.clear() + self._v6.clear() + self._aliases.clear() + for line in lines: + parts = line.split() + if len(parts) < 2: + continue + ip = parts.pop(0) + if is_ipv4_addr(ip): + ipmap = self._v4 + elif is_ipv6_addr(ip): + if ip.startswith('fe80'): + # Do not use link-local addresses, OSX stores these here + continue + ipmap = self._v6 + else: + continue + cname = parts.pop(0).lower() + ipmap[cname] = ip + for alias in parts: + alias = alias.lower() + ipmap[alias] = ip + self._aliases[alias] = cname + self._last_load = time.time() + + def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, + tcp=False, source=None, raise_on_no_answer=True): + """Query the hosts file + + The known rdtypes are dns.rdatatype.A, dns.rdatatype.AAAA and + dns.rdatatype.CNAME. + + The ``rdclass`` parameter must be dns.rdataclass.IN while the + ``tcp`` and ``source`` parameters are ignored. + + Return a HostAnswer instance or raise a dns.resolver.NoAnswer + exception. + """ + now = time.time() + if self._last_load + self.interval < now: + self._load() + rdclass = dns.rdataclass.IN + if isinstance(qname, str): + name = qname + qname = dns.name.from_text(qname) + elif isinstance(qname, bytes): + name = qname.decode("ascii") + qname = dns.name.from_text(qname) + else: + name = str(qname) + name = name.lower() + rrset = dns.rrset.RRset(qname, rdclass, rdtype) + rrset.ttl = self._last_load + self.interval - now + if rdclass == dns.rdataclass.IN and rdtype == dns.rdatatype.A: + addr = self._v4.get(name) + if not addr and qname.is_absolute(): + addr = self._v4.get(name[:-1]) + if addr: + rrset.add(dns.rdtypes.IN.A.A(rdclass, rdtype, addr)) + elif rdclass == dns.rdataclass.IN and rdtype == dns.rdatatype.AAAA: + addr = self._v6.get(name) + if not addr and qname.is_absolute(): + addr = self._v6.get(name[:-1]) + if addr: + rrset.add(dns.rdtypes.IN.AAAA.AAAA(rdclass, rdtype, addr)) + elif rdclass == dns.rdataclass.IN and rdtype == dns.rdatatype.CNAME: + cname = self._aliases.get(name) + if not cname and qname.is_absolute(): + cname = self._aliases.get(name[:-1]) + if cname: + rrset.add(dns.rdtypes.ANY.CNAME.CNAME( + rdclass, rdtype, dns.name.from_text(cname))) + return HostsAnswer(qname, rdtype, rdclass, rrset, raise_on_no_answer) + + def getaliases(self, hostname): + """Return a list of all the aliases of a given cname""" + # Due to the way store aliases this is a bit inefficient, this + # clearly was an afterthought. But this is only used by + # gethostbyname_ex so it's probably fine. + aliases = [] + if hostname in self._aliases: + cannon = self._aliases[hostname] + else: + cannon = hostname + aliases.append(cannon) + for alias, cname in self._aliases.items(): + if cannon == cname: + aliases.append(alias) + aliases.remove(hostname) + return aliases + + +class ResolverProxy: + """Resolver class which can also use /etc/hosts + + Initialise with a HostsResolver instance in order for it to also + use the hosts file. + """ + + def __init__(self, hosts_resolver=None, filename='/etc/resolv.conf'): + """Initialise the resolver proxy + + :param hosts_resolver: An instance of HostsResolver to use. + + :param filename: The filename containing the resolver + configuration. The default value is correct for both UNIX + and Windows, on Windows it will result in the configuration + being read from the Windows registry. + """ + self._hosts = hosts_resolver + self._filename = filename + # NOTE(dtantsur): we cannot create a resolver here since this code is + # executed on eventlet import. In an environment without DNS, creating + # a Resolver will fail making eventlet unusable at all. See + # https://github.com/eventlet/eventlet/issues/736 for details. + self._cached_resolver = None + + @property + def _resolver(self): + if self._cached_resolver is None: + self.clear() + return self._cached_resolver + + @_resolver.setter + def _resolver(self, value): + self._cached_resolver = value + + def clear(self): + self._resolver = dns.resolver.Resolver(filename=self._filename) + self._resolver.cache = dns.resolver.LRUCache() + + def query(self, qname, rdtype=dns.rdatatype.A, rdclass=dns.rdataclass.IN, + tcp=False, source=None, raise_on_no_answer=True, + _hosts_rdtypes=(dns.rdatatype.A, dns.rdatatype.AAAA), + use_network=True): + """Query the resolver, using /etc/hosts if enabled. + + Behavior: + 1. if hosts is enabled and contains answer, return it now + 2. query nameservers for qname if use_network is True + 3. if qname did not contain dots, pretend it was top-level domain, + query "foobar." and append to previous result + """ + result = [None, None, 0] + + if qname is None: + qname = '0.0.0.0' + if isinstance(qname, str) or isinstance(qname, bytes): + qname = dns.name.from_text(qname, None) + + def step(fun, *args, **kwargs): + try: + a = fun(*args, **kwargs) + except Exception as e: + result[1] = e + return False + if a.rrset is not None and len(a.rrset): + if result[0] is None: + result[0] = a + else: + result[0].rrset.union_update(a.rrset) + result[2] += len(a.rrset) + return True + + def end(): + if result[0] is not None: + if raise_on_no_answer and result[2] == 0: + raise dns.resolver.NoAnswer + return result[0] + if result[1] is not None: + if raise_on_no_answer or not isinstance(result[1], dns.resolver.NoAnswer): + raise result[1] + raise dns.resolver.NXDOMAIN(qnames=(qname,)) + + if (self._hosts and (rdclass == dns.rdataclass.IN) and (rdtype in _hosts_rdtypes)): + if step(self._hosts.query, qname, rdtype, raise_on_no_answer=False): + if (result[0] is not None) or (result[1] is not None) or (not use_network): + return end() + + # Main query + step(self._resolver.query, qname, rdtype, rdclass, tcp, source, raise_on_no_answer=False) + + # `resolv.conf` docs say unqualified names must resolve from search (or local) domain. + # However, common OS `getaddrinfo()` implementations append trailing dot (e.g. `db -> db.`) + # and ask nameservers, as if top-level domain was queried. + # This step follows established practice. + # https://github.com/nameko/nameko/issues/392 + # https://github.com/eventlet/eventlet/issues/363 + if len(qname) == 1: + step(self._resolver.query, qname.concatenate(dns.name.root), + rdtype, rdclass, tcp, source, raise_on_no_answer=False) + + return end() + + def getaliases(self, hostname): + """Return a list of all the aliases of a given hostname""" + if self._hosts: + aliases = self._hosts.getaliases(hostname) + else: + aliases = [] + while True: + try: + ans = self._resolver.query(hostname, dns.rdatatype.CNAME) + except (dns.resolver.NoAnswer, dns.resolver.NXDOMAIN): + break + else: + aliases.extend(str(rr.target) for rr in ans.rrset) + hostname = ans[0].target + return aliases + + +resolver = ResolverProxy(hosts_resolver=HostsResolver()) + + +def resolve(name, family=socket.AF_INET, raises=True, _proxy=None, + use_network=True): + """Resolve a name for a given family using the global resolver proxy. + + This method is called by the global getaddrinfo() function. If use_network + is False, only resolution via hosts file will be performed. + + Return a dns.resolver.Answer instance. If there is no answer it's + rrset will be emtpy. + """ + if family == socket.AF_INET: + rdtype = dns.rdatatype.A + elif family == socket.AF_INET6: + rdtype = dns.rdatatype.AAAA + else: + raise socket.gaierror(socket.EAI_FAMILY, + 'Address family not supported') + + if _proxy is None: + _proxy = resolver + try: + try: + return _proxy.query(name, rdtype, raise_on_no_answer=raises, + use_network=use_network) + except dns.resolver.NXDOMAIN: + if not raises: + return HostsAnswer(dns.name.Name(name), + rdtype, dns.rdataclass.IN, None, False) + raise + except dns.exception.Timeout: + _raise_new_error(EAI_EAGAIN_ERROR) + except dns.exception.DNSException: + _raise_new_error(EAI_NODATA_ERROR) + + +def resolve_cname(host): + """Return the canonical name of a hostname""" + try: + ans = resolver.query(host, dns.rdatatype.CNAME) + except dns.resolver.NoAnswer: + return host + except dns.exception.Timeout: + _raise_new_error(EAI_EAGAIN_ERROR) + except dns.exception.DNSException: + _raise_new_error(EAI_NODATA_ERROR) + else: + return str(ans[0].target) + + +def getaliases(host): + """Return a list of for aliases for the given hostname + + This method does translate the dnspython exceptions into + socket.gaierror exceptions. If no aliases are available an empty + list will be returned. + """ + try: + return resolver.getaliases(host) + except dns.exception.Timeout: + _raise_new_error(EAI_EAGAIN_ERROR) + except dns.exception.DNSException: + _raise_new_error(EAI_NODATA_ERROR) + + +def _getaddrinfo_lookup(host, family, flags): + """Resolve a hostname to a list of addresses + + Helper function for getaddrinfo. + """ + if flags & socket.AI_NUMERICHOST: + _raise_new_error(EAI_NONAME_ERROR) + addrs = [] + if family == socket.AF_UNSPEC: + err = None + for use_network in [False, True]: + for qfamily in [socket.AF_INET6, socket.AF_INET]: + try: + answer = resolve(host, qfamily, False, use_network=use_network) + except socket.gaierror as e: + if e.errno not in (socket.EAI_AGAIN, EAI_NONAME_ERROR.errno, EAI_NODATA_ERROR.errno): + raise + err = e + else: + if answer.rrset: + addrs.extend(rr.address for rr in answer.rrset) + if addrs: + break + if err is not None and not addrs: + raise err + elif family == socket.AF_INET6 and flags & socket.AI_V4MAPPED: + answer = resolve(host, socket.AF_INET6, False) + if answer.rrset: + addrs = [rr.address for rr in answer.rrset] + if not addrs or flags & socket.AI_ALL: + answer = resolve(host, socket.AF_INET, False) + if answer.rrset: + addrs = ['::ffff:' + rr.address for rr in answer.rrset] + else: + answer = resolve(host, family, False) + if answer.rrset: + addrs = [rr.address for rr in answer.rrset] + return str(answer.qname), addrs + + +def getaddrinfo(host, port, family=0, type=0, proto=0, flags=0): + """Replacement for Python's socket.getaddrinfo + + This does the A and AAAA lookups asynchronously after which it + calls the OS' getaddrinfo(3) using the AI_NUMERICHOST flag. This + flag ensures getaddrinfo(3) does not use the network itself and + allows us to respect all the other arguments like the native OS. + """ + if isinstance(host, str): + host = host.encode('idna').decode('ascii') + elif isinstance(host, bytes): + host = host.decode("ascii") + if host is not None and not is_ip_addr(host): + qname, addrs = _getaddrinfo_lookup(host, family, flags) + else: + qname = host + addrs = [host] + aiflags = (flags | socket.AI_NUMERICHOST) & (0xffff ^ socket.AI_CANONNAME) + res = [] + err = None + for addr in addrs: + try: + ai = socket.getaddrinfo(addr, port, family, + type, proto, aiflags) + except OSError as e: + if flags & socket.AI_ADDRCONFIG: + err = e + continue + raise + res.extend(ai) + if not res: + if err: + raise err + raise socket.gaierror(socket.EAI_NONAME, 'No address found') + if flags & socket.AI_CANONNAME: + if not is_ip_addr(qname): + qname = resolve_cname(qname).encode('ascii').decode('idna') + ai = res[0] + res[0] = (ai[0], ai[1], ai[2], qname, ai[4]) + return res + + +def gethostbyname(hostname): + """Replacement for Python's socket.gethostbyname""" + if is_ipv4_addr(hostname): + return hostname + rrset = resolve(hostname) + return rrset[0].address + + +def gethostbyname_ex(hostname): + """Replacement for Python's socket.gethostbyname_ex""" + if is_ipv4_addr(hostname): + return (hostname, [], [hostname]) + ans = resolve(hostname) + aliases = getaliases(hostname) + addrs = [rr.address for rr in ans.rrset] + qname = str(ans.qname) + if qname[-1] == '.': + qname = qname[:-1] + return (qname, aliases, addrs) + + +def getnameinfo(sockaddr, flags): + """Replacement for Python's socket.getnameinfo. + + Currently only supports IPv4. + """ + try: + host, port = sockaddr + except (ValueError, TypeError): + if not isinstance(sockaddr, tuple): + del sockaddr # to pass a stdlib test that is + # hyper-careful about reference counts + raise TypeError('getnameinfo() argument 1 must be a tuple') + else: + # must be ipv6 sockaddr, pretending we don't know how to resolve it + _raise_new_error(EAI_NONAME_ERROR) + + if (flags & socket.NI_NAMEREQD) and (flags & socket.NI_NUMERICHOST): + # Conflicting flags. Punt. + _raise_new_error(EAI_NONAME_ERROR) + + if is_ipv4_addr(host): + try: + rrset = resolver.query( + dns.reversename.from_address(host), dns.rdatatype.PTR) + if len(rrset) > 1: + raise OSError('sockaddr resolved to multiple addresses') + host = rrset[0].target.to_text(omit_final_dot=True) + except dns.exception.Timeout: + if flags & socket.NI_NAMEREQD: + _raise_new_error(EAI_EAGAIN_ERROR) + except dns.exception.DNSException: + if flags & socket.NI_NAMEREQD: + _raise_new_error(EAI_NONAME_ERROR) + else: + try: + rrset = resolver.query(host) + if len(rrset) > 1: + raise OSError('sockaddr resolved to multiple addresses') + if flags & socket.NI_NUMERICHOST: + host = rrset[0].address + except dns.exception.Timeout: + _raise_new_error(EAI_EAGAIN_ERROR) + except dns.exception.DNSException: + raise socket.gaierror( + (socket.EAI_NODATA, 'No address associated with hostname')) + + if not (flags & socket.NI_NUMERICSERV): + proto = (flags & socket.NI_DGRAM) and 'udp' or 'tcp' + port = socket.getservbyport(port, proto) + + return (host, port) + + +def _net_read(sock, count, expiration): + """coro friendly replacement for dns.query._net_read + Read the specified number of bytes from sock. Keep trying until we + either get the desired amount, or we hit EOF. + A Timeout exception will be raised if the operation is not completed + by the expiration time. + """ + s = bytearray() + while count > 0: + try: + n = sock.recv(count) + except socket.timeout: + # Q: Do we also need to catch coro.CoroutineSocketWake and pass? + if expiration - time.time() <= 0.0: + raise dns.exception.Timeout + eventlet.sleep(0.01) + continue + if n == b'': + raise EOFError + count = count - len(n) + s += n + return s + + +def _net_write(sock, data, expiration): + """coro friendly replacement for dns.query._net_write + Write the specified data to the socket. + A Timeout exception will be raised if the operation is not completed + by the expiration time. + """ + current = 0 + l = len(data) + while current < l: + try: + current += sock.send(data[current:]) + except socket.timeout: + # Q: Do we also need to catch coro.CoroutineSocketWake and pass? + if expiration - time.time() <= 0.0: + raise dns.exception.Timeout + + +# Test if raise_on_truncation is an argument we should handle. +# It was newly added in dnspython 2.0 +try: + dns.message.from_wire("", raise_on_truncation=True) +except dns.message.ShortHeader: + _handle_raise_on_truncation = True +except TypeError: + # Argument error, there is no argument "raise_on_truncation" + _handle_raise_on_truncation = False + + +def udp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53, + af=None, source=None, source_port=0, ignore_unexpected=False, + one_rr_per_rrset=False, ignore_trailing=False, + raise_on_truncation=False, sock=None, ignore_errors=False): + """coro friendly replacement for dns.query.udp + Return the response obtained after sending a query via UDP. + + @param q: the query + @type q: dns.message.Message + @param where: where to send the message + @type where: string containing an IPv4 or IPv6 address + @param timeout: The number of seconds to wait before the query times out. + If None, the default, wait forever. + @type timeout: float + @param port: The port to which to send the message. The default is 53. + @type port: int + @param af: the address family to use. The default is None, which + causes the address family to use to be inferred from the form of of where. + If the inference attempt fails, AF_INET is used. + @type af: int + @rtype: dns.message.Message object + @param source: source address. The default is the IPv4 wildcard address. + @type source: string + @param source_port: The port from which to send the message. + The default is 0. + @type source_port: int + @param ignore_unexpected: If True, ignore responses from unexpected + sources. The default is False. + @type ignore_unexpected: bool + @param one_rr_per_rrset: If True, put each RR into its own + RRset. + @type one_rr_per_rrset: bool + @param ignore_trailing: If True, ignore trailing + junk at end of the received message. + @type ignore_trailing: bool + @param raise_on_truncation: If True, raise an exception if + the TC bit is set. + @type raise_on_truncation: bool + @param sock: the socket to use for the + query. If None, the default, a socket is created. Note that + if a socket is provided, it must be a nonblocking datagram socket, + and the source and source_port are ignored. + @type sock: socket.socket | None + @param ignore_errors: if various format errors or response mismatches occur, + continue listening. + @type ignore_errors: bool""" + + wire = q.to_wire() + if af is None: + try: + af = dns.inet.af_for_address(where) + except: + af = dns.inet.AF_INET + if af == dns.inet.AF_INET: + destination = (where, port) + if source is not None: + source = (source, source_port) + elif af == dns.inet.AF_INET6: + # Purge any stray zeroes in source address. When doing the tuple comparison + # below, we need to always ensure both our target and where we receive replies + # from are compared with all zeroes removed so that we don't erroneously fail. + # e.g. ('00::1', 53, 0, 0) != ('::1', 53, 0, 0) + where_trunc = dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(where)) + destination = (where_trunc, port, 0, 0) + if source is not None: + source = (source, source_port, 0, 0) + + if sock: + s = sock + else: + s = socket.socket(af, socket.SOCK_DGRAM) + s.settimeout(timeout) + try: + expiration = compute_expiration(dns.query, timeout) + if source is not None: + s.bind(source) + while True: + try: + s.sendto(wire, destination) + break + except socket.timeout: + # Q: Do we also need to catch coro.CoroutineSocketWake and pass? + if expiration - time.time() <= 0.0: + raise dns.exception.Timeout + eventlet.sleep(0.01) + continue + + tried = False + while True: + # If we've tried to receive at least once, check to see if our + # timer expired + if tried and (expiration - time.time() <= 0.0): + raise dns.exception.Timeout + # Sleep if we are retrying the operation due to a bad source + # address or a socket timeout. + if tried: + eventlet.sleep(0.01) + tried = True + + try: + (wire, from_address) = s.recvfrom(65535) + except socket.timeout: + # Q: Do we also need to catch coro.CoroutineSocketWake and pass? + continue + if dns.inet.af_for_address(from_address[0]) == dns.inet.AF_INET6: + # Purge all possible zeroes for ipv6 to match above logic + addr = from_address[0] + addr = dns.ipv6.inet_ntoa(dns.ipv6.inet_aton(addr)) + from_address = (addr, from_address[1], from_address[2], from_address[3]) + if from_address != destination: + if ignore_unexpected: + continue + else: + raise dns.query.UnexpectedSource( + 'got a response from %s instead of %s' + % (from_address, destination)) + try: + if _handle_raise_on_truncation: + r = dns.message.from_wire(wire, + keyring=q.keyring, + request_mac=q.mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing, + raise_on_truncation=raise_on_truncation) + else: + r = dns.message.from_wire(wire, + keyring=q.keyring, + request_mac=q.mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing) + if not q.is_response(r): + raise dns.query.BadResponse() + break + except dns.message.Truncated as e: + if ignore_errors and not q.is_response(e.message()): + continue + else: + raise + except Exception: + if ignore_errors: + continue + else: + raise + finally: + s.close() + + return r + + +def tcp(q, where, timeout=DNS_QUERY_TIMEOUT, port=53, + af=None, source=None, source_port=0, + one_rr_per_rrset=False, ignore_trailing=False, sock=None): + """coro friendly replacement for dns.query.tcp + Return the response obtained after sending a query via TCP. + + @param q: the query + @type q: dns.message.Message object + @param where: where to send the message + @type where: string containing an IPv4 or IPv6 address + @param timeout: The number of seconds to wait before the query times out. + If None, the default, wait forever. + @type timeout: float + @param port: The port to which to send the message. The default is 53. + @type port: int + @param af: the address family to use. The default is None, which + causes the address family to use to be inferred from the form of of where. + If the inference attempt fails, AF_INET is used. + @type af: int + @rtype: dns.message.Message object + @param source: source address. The default is the IPv4 wildcard address. + @type source: string + @param source_port: The port from which to send the message. + The default is 0. + @type source_port: int + @type ignore_unexpected: bool + @param one_rr_per_rrset: If True, put each RR into its own + RRset. + @type one_rr_per_rrset: bool + @param ignore_trailing: If True, ignore trailing + junk at end of the received message. + @type ignore_trailing: bool + @param sock: the socket to use for the + query. If None, the default, a socket is created. Note that + if a socket is provided, it must be a nonblocking datagram socket, + and the source and source_port are ignored. + @type sock: socket.socket | None""" + + wire = q.to_wire() + if af is None: + try: + af = dns.inet.af_for_address(where) + except: + af = dns.inet.AF_INET + if af == dns.inet.AF_INET: + destination = (where, port) + if source is not None: + source = (source, source_port) + elif af == dns.inet.AF_INET6: + destination = (where, port, 0, 0) + if source is not None: + source = (source, source_port, 0, 0) + if sock: + s = sock + else: + s = socket.socket(af, socket.SOCK_STREAM) + s.settimeout(timeout) + try: + expiration = compute_expiration(dns.query, timeout) + if source is not None: + s.bind(source) + while True: + try: + s.connect(destination) + break + except socket.timeout: + # Q: Do we also need to catch coro.CoroutineSocketWake and pass? + if expiration - time.time() <= 0.0: + raise dns.exception.Timeout + eventlet.sleep(0.01) + continue + + l = len(wire) + # copying the wire into tcpmsg is inefficient, but lets us + # avoid writev() or doing a short write that would get pushed + # onto the net + tcpmsg = struct.pack("!H", l) + wire + _net_write(s, tcpmsg, expiration) + ldata = _net_read(s, 2, expiration) + (l,) = struct.unpack("!H", ldata) + wire = bytes(_net_read(s, l, expiration)) + finally: + s.close() + r = dns.message.from_wire(wire, keyring=q.keyring, request_mac=q.mac, + one_rr_per_rrset=one_rr_per_rrset, + ignore_trailing=ignore_trailing) + if not q.is_response(r): + raise dns.query.BadResponse() + return r + + +def reset(): + resolver.clear() + + +# Install our coro-friendly replacements for the tcp and udp query methods. +dns.query.tcp = tcp +dns.query.udp = udp diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/support/greenlets.py b/netdeploy/lib/python3.11/site-packages/eventlet/support/greenlets.py new file mode 100644 index 0000000..b939328 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/support/greenlets.py @@ -0,0 +1,4 @@ +import greenlet +getcurrent = greenlet.greenlet.getcurrent +GreenletExit = greenlet.greenlet.GreenletExit +greenlet = greenlet.greenlet diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/support/psycopg2_patcher.py b/netdeploy/lib/python3.11/site-packages/eventlet/support/psycopg2_patcher.py new file mode 100644 index 0000000..2f4034a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/support/psycopg2_patcher.py @@ -0,0 +1,55 @@ +"""A wait callback to allow psycopg2 cooperation with eventlet. + +Use `make_psycopg_green()` to enable eventlet support in Psycopg. +""" + +# Copyright (C) 2010 Daniele Varrazzo +# and licensed under the MIT license: +# +# 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. + +import psycopg2 +from psycopg2 import extensions + +import eventlet.hubs + + +def make_psycopg_green(): + """Configure Psycopg to be used with eventlet in non-blocking way.""" + if not hasattr(extensions, 'set_wait_callback'): + raise ImportError( + "support for coroutines not available in this Psycopg version (%s)" + % psycopg2.__version__) + + extensions.set_wait_callback(eventlet_wait_callback) + + +def eventlet_wait_callback(conn, timeout=-1): + """A wait callback useful to allow eventlet to work with Psycopg.""" + while 1: + state = conn.poll() + if state == extensions.POLL_OK: + break + elif state == extensions.POLL_READ: + eventlet.hubs.trampoline(conn.fileno(), read=True) + elif state == extensions.POLL_WRITE: + eventlet.hubs.trampoline(conn.fileno(), write=True) + else: + raise psycopg2.OperationalError( + "Bad result from poll: %r" % state) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/support/pylib.py b/netdeploy/lib/python3.11/site-packages/eventlet/support/pylib.py new file mode 100644 index 0000000..fdb0682 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/support/pylib.py @@ -0,0 +1,12 @@ +from py.magic import greenlet + +import sys +import types + + +def emulate(): + module = types.ModuleType('greenlet') + sys.modules['greenlet'] = module + module.greenlet = greenlet + module.getcurrent = greenlet.getcurrent + module.GreenletExit = greenlet.GreenletExit diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/support/stacklesspypys.py b/netdeploy/lib/python3.11/site-packages/eventlet/support/stacklesspypys.py new file mode 100644 index 0000000..fe3638a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/support/stacklesspypys.py @@ -0,0 +1,12 @@ +from stackless import greenlet + +import sys +import types + + +def emulate(): + module = types.ModuleType('greenlet') + sys.modules['greenlet'] = module + module.greenlet = greenlet + module.getcurrent = greenlet.getcurrent + module.GreenletExit = greenlet.GreenletExit diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/support/stacklesss.py b/netdeploy/lib/python3.11/site-packages/eventlet/support/stacklesss.py new file mode 100644 index 0000000..9b3951e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/support/stacklesss.py @@ -0,0 +1,84 @@ +""" +Support for using stackless python. Broken and riddled with print statements +at the moment. Please fix it! +""" + +import sys +import types + +import stackless + +caller = None +coro_args = {} +tasklet_to_greenlet = {} + + +def getcurrent(): + return tasklet_to_greenlet[stackless.getcurrent()] + + +class FirstSwitch: + def __init__(self, gr): + self.gr = gr + + def __call__(self, *args, **kw): + # print("first call", args, kw) + gr = self.gr + del gr.switch + run, gr.run = gr.run, None + t = stackless.tasklet(run) + gr.t = t + tasklet_to_greenlet[t] = gr + t.setup(*args, **kw) + t.run() + + +class greenlet: + def __init__(self, run=None, parent=None): + self.dead = False + if parent is None: + parent = getcurrent() + + self.parent = parent + if run is not None: + self.run = run + + self.switch = FirstSwitch(self) + + def switch(self, *args): + # print("switch", args) + global caller + caller = stackless.getcurrent() + coro_args[self] = args + self.t.insert() + stackless.schedule() + if caller is not self.t: + caller.remove() + rval = coro_args[self] + return rval + + def run(self): + pass + + def __bool__(self): + return self.run is None and not self.dead + + +class GreenletExit(Exception): + pass + + +def emulate(): + module = types.ModuleType('greenlet') + sys.modules['greenlet'] = module + module.greenlet = greenlet + module.getcurrent = getcurrent + module.GreenletExit = GreenletExit + + caller = stackless.getcurrent() + tasklet_to_greenlet[caller] = None + main_coro = greenlet() + tasklet_to_greenlet[caller] = main_coro + main_coro.t = caller + del main_coro.switch # It's already running + coro_args[main_coro] = None diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/timeout.py b/netdeploy/lib/python3.11/site-packages/eventlet/timeout.py new file mode 100644 index 0000000..4ab893e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/timeout.py @@ -0,0 +1,184 @@ +# Copyright (c) 2009-2010 Denis Bilenko, denis.bilenko at gmail com +# Copyright (c) 2010 Eventlet Contributors (see AUTHORS) +# and licensed under the MIT license: +# +# 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. + +import functools +import inspect + +import eventlet +from eventlet.support import greenlets as greenlet +from eventlet.hubs import get_hub + +__all__ = ['Timeout', 'with_timeout', 'wrap_is_timeout', 'is_timeout'] + +_MISSING = object() + +# deriving from BaseException so that "except Exception as e" doesn't catch +# Timeout exceptions. + + +class Timeout(BaseException): + """Raises *exception* in the current greenthread after *timeout* seconds. + + When *exception* is omitted or ``None``, the :class:`Timeout` instance + itself is raised. If *seconds* is None, the timer is not scheduled, and is + only useful if you're planning to raise it directly. + + Timeout objects are context managers, and so can be used in with statements. + When used in a with statement, if *exception* is ``False``, the timeout is + still raised, but the context manager suppresses it, so the code outside the + with-block won't see it. + """ + + def __init__(self, seconds=None, exception=None): + self.seconds = seconds + self.exception = exception + self.timer = None + self.start() + + def start(self): + """Schedule the timeout. This is called on construction, so + it should not be called explicitly, unless the timer has been + canceled.""" + assert not self.pending, \ + '%r is already started; to restart it, cancel it first' % self + if self.seconds is None: # "fake" timeout (never expires) + self.timer = None + elif self.exception is None or isinstance(self.exception, bool): # timeout that raises self + self.timer = get_hub().schedule_call_global( + self.seconds, greenlet.getcurrent().throw, self) + else: # regular timeout with user-provided exception + self.timer = get_hub().schedule_call_global( + self.seconds, greenlet.getcurrent().throw, self.exception) + return self + + @property + def pending(self): + """True if the timeout is scheduled to be raised.""" + if self.timer is not None: + return self.timer.pending + else: + return False + + def cancel(self): + """If the timeout is pending, cancel it. If not using + Timeouts in ``with`` statements, always call cancel() in a + ``finally`` after the block of code that is getting timed out. + If not canceled, the timeout will be raised later on, in some + unexpected section of the application.""" + if self.timer is not None: + self.timer.cancel() + self.timer = None + + def __repr__(self): + classname = self.__class__.__name__ + if self.pending: + pending = ' pending' + else: + pending = '' + if self.exception is None: + exception = '' + else: + exception = ' exception=%r' % self.exception + return '<%s at %s seconds=%s%s%s>' % ( + classname, hex(id(self)), self.seconds, exception, pending) + + def __str__(self): + """ + >>> raise Timeout # doctest: +IGNORE_EXCEPTION_DETAIL + Traceback (most recent call last): + ... + Timeout + """ + if self.seconds is None: + return '' + if self.seconds == 1: + suffix = '' + else: + suffix = 's' + if self.exception is None or self.exception is True: + return '%s second%s' % (self.seconds, suffix) + elif self.exception is False: + return '%s second%s (silent)' % (self.seconds, suffix) + else: + return '%s second%s (%s)' % (self.seconds, suffix, self.exception) + + def __enter__(self): + if self.timer is None: + self.start() + return self + + def __exit__(self, typ, value, tb): + self.cancel() + if value is self and self.exception is False: + return True + + @property + def is_timeout(self): + return True + + +def with_timeout(seconds, function, *args, **kwds): + """Wrap a call to some (yielding) function with a timeout; if the called + function fails to return before the timeout, cancel it and return a flag + value. + """ + timeout_value = kwds.pop("timeout_value", _MISSING) + timeout = Timeout(seconds) + try: + try: + return function(*args, **kwds) + except Timeout as ex: + if ex is timeout and timeout_value is not _MISSING: + return timeout_value + raise + finally: + timeout.cancel() + + +def wrap_is_timeout(base): + '''Adds `.is_timeout=True` attribute to objects returned by `base()`. + + When `base` is class, attribute is added as read-only property. Returns `base`. + Otherwise, it returns a function that sets attribute on result of `base()` call. + + Wrappers make best effort to be transparent. + ''' + if inspect.isclass(base): + base.is_timeout = property(lambda _: True) + return base + + @functools.wraps(base) + def fun(*args, **kwargs): + ex = base(*args, **kwargs) + ex.is_timeout = True + return ex + return fun + + +if isinstance(__builtins__, dict): # seen when running tests on py310, but HOW?? + _timeout_err = __builtins__.get('TimeoutError', Timeout) +else: + _timeout_err = getattr(__builtins__, 'TimeoutError', Timeout) + + +def is_timeout(obj): + return bool(getattr(obj, 'is_timeout', False)) or isinstance(obj, _timeout_err) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/tpool.py b/netdeploy/lib/python3.11/site-packages/eventlet/tpool.py new file mode 100644 index 0000000..1a3f412 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/tpool.py @@ -0,0 +1,336 @@ +# Copyright (c) 2007-2009, Linden Research, Inc. +# Copyright (c) 2007, IBM Corp. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import atexit +try: + import _imp as imp +except ImportError: + import imp +import os +import sys +import traceback + +import eventlet +from eventlet import event, greenio, greenthread, patcher, timeout + +__all__ = ['execute', 'Proxy', 'killall', 'set_num_threads'] + + +EXC_CLASSES = (Exception, timeout.Timeout) +SYS_EXCS = (GeneratorExit, KeyboardInterrupt, SystemExit) + +QUIET = True + +socket = patcher.original('socket') +threading = patcher.original('threading') +Queue_module = patcher.original('queue') + +Empty = Queue_module.Empty +Queue = Queue_module.Queue + +_bytetosend = b' ' +_coro = None +_nthreads = int(os.environ.get('EVENTLET_THREADPOOL_SIZE', 20)) +_reqq = _rspq = None +_rsock = _wsock = None +_setup_already = False +_threads = [] + + +def tpool_trampoline(): + global _rspq + while True: + try: + _c = _rsock.recv(1) + assert _c + # FIXME: this is probably redundant since using sockets instead of pipe now + except ValueError: + break # will be raised when pipe is closed + while not _rspq.empty(): + try: + (e, rv) = _rspq.get(block=False) + e.send(rv) + e = rv = None + except Empty: + pass + + +def tworker(): + global _rspq + while True: + try: + msg = _reqq.get() + except AttributeError: + return # can't get anything off of a dud queue + if msg is None: + return + (e, meth, args, kwargs) = msg + rv = None + try: + rv = meth(*args, **kwargs) + except SYS_EXCS: + raise + except EXC_CLASSES: + rv = sys.exc_info() + traceback.clear_frames(rv[1].__traceback__) + # test_leakage_from_tracebacks verifies that the use of + # exc_info does not lead to memory leaks + _rspq.put((e, rv)) + msg = meth = args = kwargs = e = rv = None + _wsock.sendall(_bytetosend) + + +def execute(meth, *args, **kwargs): + """ + Execute *meth* in a Python thread, blocking the current coroutine/ + greenthread until the method completes. + + The primary use case for this is to wrap an object or module that is not + amenable to monkeypatching or any of the other tricks that Eventlet uses + to achieve cooperative yielding. With tpool, you can force such objects to + cooperate with green threads by sticking them in native threads, at the cost + of some overhead. + """ + setup() + # if already in tpool, don't recurse into the tpool + # also, call functions directly if we're inside an import lock, because + # if meth does any importing (sadly common), it will hang + my_thread = threading.current_thread() + if my_thread in _threads or imp.lock_held() or _nthreads == 0: + return meth(*args, **kwargs) + + e = event.Event() + _reqq.put((e, meth, args, kwargs)) + + rv = e.wait() + if isinstance(rv, tuple) \ + and len(rv) == 3 \ + and isinstance(rv[1], EXC_CLASSES): + (c, e, tb) = rv + if not QUIET: + traceback.print_exception(c, e, tb) + traceback.print_stack() + raise e.with_traceback(tb) + return rv + + +def proxy_call(autowrap, f, *args, **kwargs): + """ + Call a function *f* and returns the value. If the type of the return value + is in the *autowrap* collection, then it is wrapped in a :class:`Proxy` + object before return. + + Normally *f* will be called in the threadpool with :func:`execute`; if the + keyword argument "nonblocking" is set to ``True``, it will simply be + executed directly. This is useful if you have an object which has methods + that don't need to be called in a separate thread, but which return objects + that should be Proxy wrapped. + """ + if kwargs.pop('nonblocking', False): + rv = f(*args, **kwargs) + else: + rv = execute(f, *args, **kwargs) + if isinstance(rv, autowrap): + return Proxy(rv, autowrap) + else: + return rv + + +class Proxy: + """ + a simple proxy-wrapper of any object that comes with a + methods-only interface, in order to forward every method + invocation onto a thread in the native-thread pool. A key + restriction is that the object's methods should not switch + greenlets or use Eventlet primitives, since they are in a + different thread from the main hub, and therefore might behave + unexpectedly. This is for running native-threaded code + only. + + It's common to want to have some of the attributes or return + values also wrapped in Proxy objects (for example, database + connection objects produce cursor objects which also should be + wrapped in Proxy objects to remain nonblocking). *autowrap*, if + supplied, is a collection of types; if an attribute or return + value matches one of those types (via isinstance), it will be + wrapped in a Proxy. *autowrap_names* is a collection + of strings, which represent the names of attributes that should be + wrapped in Proxy objects when accessed. + """ + + def __init__(self, obj, autowrap=(), autowrap_names=()): + self._obj = obj + self._autowrap = autowrap + self._autowrap_names = autowrap_names + + def __getattr__(self, attr_name): + f = getattr(self._obj, attr_name) + if not hasattr(f, '__call__'): + if isinstance(f, self._autowrap) or attr_name in self._autowrap_names: + return Proxy(f, self._autowrap) + return f + + def doit(*args, **kwargs): + result = proxy_call(self._autowrap, f, *args, **kwargs) + if attr_name in self._autowrap_names and not isinstance(result, Proxy): + return Proxy(result) + return result + return doit + + # the following are a buncha methods that the python interpeter + # doesn't use getattr to retrieve and therefore have to be defined + # explicitly + def __getitem__(self, key): + return proxy_call(self._autowrap, self._obj.__getitem__, key) + + def __setitem__(self, key, value): + return proxy_call(self._autowrap, self._obj.__setitem__, key, value) + + def __deepcopy__(self, memo=None): + return proxy_call(self._autowrap, self._obj.__deepcopy__, memo) + + def __copy__(self, memo=None): + return proxy_call(self._autowrap, self._obj.__copy__, memo) + + def __call__(self, *a, **kw): + if '__call__' in self._autowrap_names: + return Proxy(proxy_call(self._autowrap, self._obj, *a, **kw)) + else: + return proxy_call(self._autowrap, self._obj, *a, **kw) + + def __enter__(self): + return proxy_call(self._autowrap, self._obj.__enter__) + + def __exit__(self, *exc): + return proxy_call(self._autowrap, self._obj.__exit__, *exc) + + # these don't go through a proxy call, because they're likely to + # be called often, and are unlikely to be implemented on the + # wrapped object in such a way that they would block + def __eq__(self, rhs): + return self._obj == rhs + + def __hash__(self): + return self._obj.__hash__() + + def __repr__(self): + return self._obj.__repr__() + + def __str__(self): + return self._obj.__str__() + + def __len__(self): + return len(self._obj) + + def __nonzero__(self): + return bool(self._obj) + # Python3 + __bool__ = __nonzero__ + + def __iter__(self): + it = iter(self._obj) + if it == self._obj: + return self + else: + return Proxy(it) + + def next(self): + return proxy_call(self._autowrap, next, self._obj) + # Python3 + __next__ = next + + +def setup(): + global _rsock, _wsock, _coro, _setup_already, _rspq, _reqq + if _setup_already: + return + else: + _setup_already = True + + assert _nthreads >= 0, "Can't specify negative number of threads" + if _nthreads == 0: + import warnings + warnings.warn("Zero threads in tpool. All tpool.execute calls will\ + execute in main thread. Check the value of the environment \ + variable EVENTLET_THREADPOOL_SIZE.", RuntimeWarning) + _reqq = Queue(maxsize=-1) + _rspq = Queue(maxsize=-1) + + # connected socket pair + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sock.bind(('127.0.0.1', 0)) + sock.listen(1) + csock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + csock.connect(sock.getsockname()) + csock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) + _wsock, _addr = sock.accept() + _wsock.settimeout(None) + _wsock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, True) + sock.close() + _rsock = greenio.GreenSocket(csock) + _rsock.settimeout(None) + + for i in range(_nthreads): + t = threading.Thread(target=tworker, + name="tpool_thread_%s" % i) + t.daemon = True + t.start() + _threads.append(t) + + _coro = greenthread.spawn_n(tpool_trampoline) + # This yield fixes subtle error with GreenSocket.__del__ + eventlet.sleep(0) + + +# Avoid ResourceWarning unclosed socket on Python3.2+ +@atexit.register +def killall(): + global _setup_already, _rspq, _rsock, _wsock + if not _setup_already: + return + + # This yield fixes freeze in some scenarios + eventlet.sleep(0) + + for thr in _threads: + _reqq.put(None) + for thr in _threads: + thr.join() + del _threads[:] + + # return any remaining results + while (_rspq is not None) and not _rspq.empty(): + try: + (e, rv) = _rspq.get(block=False) + e.send(rv) + e = rv = None + except Empty: + pass + + if _coro is not None: + greenthread.kill(_coro) + if _rsock is not None: + _rsock.close() + _rsock = None + if _wsock is not None: + _wsock.close() + _wsock = None + _rspq = None + _setup_already = False + + +def set_num_threads(nthreads): + global _nthreads + _nthreads = nthreads diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/websocket.py b/netdeploy/lib/python3.11/site-packages/eventlet/websocket.py new file mode 100644 index 0000000..3d50f70 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/websocket.py @@ -0,0 +1,868 @@ +import base64 +import codecs +import collections +import errno +from random import Random +from socket import error as SocketError +import string +import struct +import sys +import time + +import zlib + +try: + from hashlib import md5, sha1 +except ImportError: # pragma NO COVER + from md5 import md5 + from sha import sha as sha1 + +from eventlet import semaphore +from eventlet import wsgi +from eventlet.green import socket +from eventlet.support import get_errno + +# Python 2's utf8 decoding is more lenient than we'd like +# In order to pass autobahn's testsuite we need stricter validation +# if available... +for _mod in ('wsaccel.utf8validator', 'autobahn.utf8validator'): + # autobahn has it's own python-based validator. in newest versions + # this prefers to use wsaccel, a cython based implementation, if available. + # wsaccel may also be installed w/out autobahn, or with a earlier version. + try: + utf8validator = __import__(_mod, {}, {}, ['']) + except ImportError: + utf8validator = None + else: + break + +ACCEPTABLE_CLIENT_ERRORS = {errno.ECONNRESET, errno.EPIPE, errno.ESHUTDOWN} +DEFAULT_MAX_FRAME_LENGTH = 8 << 20 + +__all__ = ["WebSocketWSGI", "WebSocket"] +PROTOCOL_GUID = b'258EAFA5-E914-47DA-95CA-C5AB0DC85B11' +VALID_CLOSE_STATUS = set( + list(range(1000, 1004)) + + list(range(1007, 1012)) + + # 3000-3999: reserved for use by libraries, frameworks, + # and applications + list(range(3000, 4000)) + + # 4000-4999: reserved for private use and thus can't + # be registered + list(range(4000, 5000)) +) + + +class BadRequest(Exception): + def __init__(self, status='400 Bad Request', body=None, headers=None): + super(Exception, self).__init__() + self.status = status + self.body = body + self.headers = headers + + +class WebSocketWSGI: + """Wraps a websocket handler function in a WSGI application. + + Use it like this:: + + @websocket.WebSocketWSGI + def my_handler(ws): + from_browser = ws.wait() + ws.send("from server") + + The single argument to the function will be an instance of + :class:`WebSocket`. To close the socket, simply return from the + function. Note that the server will log the websocket request at + the time of closure. + + An optional argument max_frame_length can be given, which will set the + maximum incoming *uncompressed* payload length of a frame. By default, this + is set to 8MiB. Note that excessive values here might create a DOS attack + vector. + """ + + def __init__(self, handler, max_frame_length=DEFAULT_MAX_FRAME_LENGTH): + self.handler = handler + self.protocol_version = None + self.support_legacy_versions = True + self.supported_protocols = [] + self.origin_checker = None + self.max_frame_length = max_frame_length + + @classmethod + def configured(cls, + handler=None, + supported_protocols=None, + origin_checker=None, + support_legacy_versions=False): + def decorator(handler): + inst = cls(handler) + inst.support_legacy_versions = support_legacy_versions + inst.origin_checker = origin_checker + if supported_protocols: + inst.supported_protocols = supported_protocols + return inst + if handler is None: + return decorator + return decorator(handler) + + def __call__(self, environ, start_response): + http_connection_parts = [ + part.strip() + for part in environ.get('HTTP_CONNECTION', '').lower().split(',')] + if not ('upgrade' in http_connection_parts and + environ.get('HTTP_UPGRADE', '').lower() == 'websocket'): + # need to check a few more things here for true compliance + start_response('400 Bad Request', [('Connection', 'close')]) + return [] + + try: + if 'HTTP_SEC_WEBSOCKET_VERSION' in environ: + ws = self._handle_hybi_request(environ) + elif self.support_legacy_versions: + ws = self._handle_legacy_request(environ) + else: + raise BadRequest() + except BadRequest as e: + status = e.status + body = e.body or b'' + headers = e.headers or [] + start_response(status, + [('Connection', 'close'), ] + headers) + return [body] + + # We're ready to switch protocols; if running under Eventlet + # (this is not always the case) then flag the connection as + # idle to play well with a graceful stop + if 'eventlet.set_idle' in environ: + environ['eventlet.set_idle']() + try: + self.handler(ws) + except OSError as e: + if get_errno(e) not in ACCEPTABLE_CLIENT_ERRORS: + raise + # Make sure we send the closing frame + ws._send_closing_frame(True) + # use this undocumented feature of eventlet.wsgi to ensure that it + # doesn't barf on the fact that we didn't call start_response + wsgi.WSGI_LOCAL.already_handled = True + return [] + + def _handle_legacy_request(self, environ): + if 'eventlet.input' in environ: + sock = environ['eventlet.input'].get_socket() + elif 'gunicorn.socket' in environ: + sock = environ['gunicorn.socket'] + else: + raise Exception('No eventlet.input or gunicorn.socket present in environ.') + + if 'HTTP_SEC_WEBSOCKET_KEY1' in environ: + self.protocol_version = 76 + if 'HTTP_SEC_WEBSOCKET_KEY2' not in environ: + raise BadRequest() + else: + self.protocol_version = 75 + + if self.protocol_version == 76: + key1 = self._extract_number(environ['HTTP_SEC_WEBSOCKET_KEY1']) + key2 = self._extract_number(environ['HTTP_SEC_WEBSOCKET_KEY2']) + # There's no content-length header in the request, but it has 8 + # bytes of data. + environ['wsgi.input'].content_length = 8 + key3 = environ['wsgi.input'].read(8) + key = struct.pack(">II", key1, key2) + key3 + response = md5(key).digest() + + # Start building the response + scheme = 'ws' + if environ.get('wsgi.url_scheme') == 'https': + scheme = 'wss' + location = '%s://%s%s%s' % ( + scheme, + environ.get('HTTP_HOST'), + environ.get('SCRIPT_NAME'), + environ.get('PATH_INFO') + ) + qs = environ.get('QUERY_STRING') + if qs is not None: + location += '?' + qs + if self.protocol_version == 75: + handshake_reply = ( + b"HTTP/1.1 101 Web Socket Protocol Handshake\r\n" + b"Upgrade: WebSocket\r\n" + b"Connection: Upgrade\r\n" + b"WebSocket-Origin: " + environ.get('HTTP_ORIGIN').encode() + b"\r\n" + b"WebSocket-Location: " + location.encode() + b"\r\n\r\n" + ) + elif self.protocol_version == 76: + handshake_reply = ( + b"HTTP/1.1 101 WebSocket Protocol Handshake\r\n" + b"Upgrade: WebSocket\r\n" + b"Connection: Upgrade\r\n" + b"Sec-WebSocket-Origin: " + environ.get('HTTP_ORIGIN').encode() + b"\r\n" + b"Sec-WebSocket-Protocol: " + + environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL', 'default').encode() + b"\r\n" + b"Sec-WebSocket-Location: " + location.encode() + b"\r\n" + b"\r\n" + response + ) + else: # pragma NO COVER + raise ValueError("Unknown WebSocket protocol version.") + sock.sendall(handshake_reply) + return WebSocket(sock, environ, self.protocol_version) + + def _parse_extension_header(self, header): + if header is None: + return None + res = {} + for ext in header.split(","): + parts = ext.split(";") + config = {} + for part in parts[1:]: + key_val = part.split("=") + if len(key_val) == 1: + config[key_val[0].strip().lower()] = True + else: + config[key_val[0].strip().lower()] = key_val[1].strip().strip('"').lower() + res.setdefault(parts[0].strip().lower(), []).append(config) + return res + + def _negotiate_permessage_deflate(self, extensions): + if not extensions: + return None + deflate = extensions.get("permessage-deflate") + if deflate is None: + return None + for config in deflate: + # We'll evaluate each config in the client's preferred order and pick + # the first that we can support. + want_config = { + # These are bool options, we can support both + "server_no_context_takeover": config.get("server_no_context_takeover", False), + "client_no_context_takeover": config.get("client_no_context_takeover", False) + } + # These are either bool OR int options. True means the client can accept a value + # for the option, a number means the client wants that specific value. + max_wbits = min(zlib.MAX_WBITS, 15) + mwb = config.get("server_max_window_bits") + if mwb is not None: + if mwb is True: + want_config["server_max_window_bits"] = max_wbits + else: + want_config["server_max_window_bits"] = \ + int(config.get("server_max_window_bits", max_wbits)) + if not (8 <= want_config["server_max_window_bits"] <= 15): + continue + mwb = config.get("client_max_window_bits") + if mwb is not None: + if mwb is True: + want_config["client_max_window_bits"] = max_wbits + else: + want_config["client_max_window_bits"] = \ + int(config.get("client_max_window_bits", max_wbits)) + if not (8 <= want_config["client_max_window_bits"] <= 15): + continue + return want_config + return None + + def _format_extension_header(self, parsed_extensions): + if not parsed_extensions: + return None + parts = [] + for name, config in parsed_extensions.items(): + ext_parts = [name.encode()] + for key, value in config.items(): + if value is False: + pass + elif value is True: + ext_parts.append(key.encode()) + else: + ext_parts.append(("%s=%s" % (key, str(value))).encode()) + parts.append(b"; ".join(ext_parts)) + return b", ".join(parts) + + def _handle_hybi_request(self, environ): + if 'eventlet.input' in environ: + sock = environ['eventlet.input'].get_socket() + elif 'gunicorn.socket' in environ: + sock = environ['gunicorn.socket'] + else: + raise Exception('No eventlet.input or gunicorn.socket present in environ.') + + hybi_version = environ['HTTP_SEC_WEBSOCKET_VERSION'] + if hybi_version not in ('8', '13', ): + raise BadRequest(status='426 Upgrade Required', + headers=[('Sec-WebSocket-Version', '8, 13')]) + self.protocol_version = int(hybi_version) + if 'HTTP_SEC_WEBSOCKET_KEY' not in environ: + # That's bad. + raise BadRequest() + origin = environ.get( + 'HTTP_ORIGIN', + (environ.get('HTTP_SEC_WEBSOCKET_ORIGIN', '') + if self.protocol_version <= 8 else '')) + if self.origin_checker is not None: + if not self.origin_checker(environ.get('HTTP_HOST'), origin): + raise BadRequest(status='403 Forbidden') + protocols = environ.get('HTTP_SEC_WEBSOCKET_PROTOCOL', None) + negotiated_protocol = None + if protocols: + for p in (i.strip() for i in protocols.split(',')): + if p in self.supported_protocols: + negotiated_protocol = p + break + + key = environ['HTTP_SEC_WEBSOCKET_KEY'] + response = base64.b64encode(sha1(key.encode() + PROTOCOL_GUID).digest()) + handshake_reply = [b"HTTP/1.1 101 Switching Protocols", + b"Upgrade: websocket", + b"Connection: Upgrade", + b"Sec-WebSocket-Accept: " + response] + if negotiated_protocol: + handshake_reply.append(b"Sec-WebSocket-Protocol: " + negotiated_protocol.encode()) + + parsed_extensions = {} + extensions = self._parse_extension_header(environ.get("HTTP_SEC_WEBSOCKET_EXTENSIONS")) + + deflate = self._negotiate_permessage_deflate(extensions) + if deflate is not None: + parsed_extensions["permessage-deflate"] = deflate + + formatted_ext = self._format_extension_header(parsed_extensions) + if formatted_ext is not None: + handshake_reply.append(b"Sec-WebSocket-Extensions: " + formatted_ext) + + sock.sendall(b'\r\n'.join(handshake_reply) + b'\r\n\r\n') + return RFC6455WebSocket(sock, environ, self.protocol_version, + protocol=negotiated_protocol, + extensions=parsed_extensions, + max_frame_length=self.max_frame_length) + + def _extract_number(self, value): + """ + Utility function which, given a string like 'g98sd 5[]221@1', will + return 9852211. Used to parse the Sec-WebSocket-Key headers. + """ + out = "" + spaces = 0 + for char in value: + if char in string.digits: + out += char + elif char == " ": + spaces += 1 + return int(out) // spaces + + +class WebSocket: + """A websocket object that handles the details of + serialization/deserialization to the socket. + + The primary way to interact with a :class:`WebSocket` object is to + call :meth:`send` and :meth:`wait` in order to pass messages back + and forth with the browser. Also available are the following + properties: + + path + The path value of the request. This is the same as the WSGI PATH_INFO variable, + but more convenient. + protocol + The value of the Websocket-Protocol header. + origin + The value of the 'Origin' header. + environ + The full WSGI environment for this request. + + """ + + def __init__(self, sock, environ, version=76): + """ + :param socket: The eventlet socket + :type socket: :class:`eventlet.greenio.GreenSocket` + :param environ: The wsgi environment + :param version: The WebSocket spec version to follow (default is 76) + """ + self.log = environ.get('wsgi.errors', sys.stderr) + self.log_context = 'server={shost}/{spath} client={caddr}:{cport}'.format( + shost=environ.get('HTTP_HOST'), + spath=environ.get('SCRIPT_NAME', '') + environ.get('PATH_INFO', ''), + caddr=environ.get('REMOTE_ADDR'), cport=environ.get('REMOTE_PORT'), + ) + self.socket = sock + self.origin = environ.get('HTTP_ORIGIN') + self.protocol = environ.get('HTTP_WEBSOCKET_PROTOCOL') + self.path = environ.get('PATH_INFO') + self.environ = environ + self.version = version + self.websocket_closed = False + self._buf = b"" + self._msgs = collections.deque() + self._sendlock = semaphore.Semaphore() + + def _pack_message(self, message): + """Pack the message inside ``00`` and ``FF`` + + As per the dataframing section (5.3) for the websocket spec + """ + if isinstance(message, str): + message = message.encode('utf-8') + elif not isinstance(message, bytes): + message = str(message).encode() + packed = b"\x00" + message + b"\xFF" + return packed + + def _parse_messages(self): + """ Parses for messages in the buffer *buf*. It is assumed that + the buffer contains the start character for a message, but that it + may contain only part of the rest of the message. + + Returns an array of messages, and the buffer remainder that + didn't contain any full messages.""" + msgs = [] + end_idx = 0 + buf = self._buf + while buf: + frame_type = buf[0] + if frame_type == 0: + # Normal message. + end_idx = buf.find(b"\xFF") + if end_idx == -1: # pragma NO COVER + break + msgs.append(buf[1:end_idx].decode('utf-8', 'replace')) + buf = buf[end_idx + 1:] + elif frame_type == 255: + # Closing handshake. + assert buf[1] == 0, "Unexpected closing handshake: %r" % buf + self.websocket_closed = True + break + else: + raise ValueError("Don't understand how to parse this type of message: %r" % buf) + self._buf = buf + return msgs + + def send(self, message): + """Send a message to the browser. + + *message* should be convertable to a string; unicode objects should be + encodable as utf-8. Raises socket.error with errno of 32 + (broken pipe) if the socket has already been closed by the client.""" + packed = self._pack_message(message) + # if two greenthreads are trying to send at the same time + # on the same socket, sendlock prevents interleaving and corruption + self._sendlock.acquire() + try: + self.socket.sendall(packed) + finally: + self._sendlock.release() + + def wait(self): + """Waits for and deserializes messages. + + Returns a single message; the oldest not yet processed. If the client + has already closed the connection, returns None. This is different + from normal socket behavior because the empty string is a valid + websocket message.""" + while not self._msgs: + # Websocket might be closed already. + if self.websocket_closed: + return None + # no parsed messages, must mean buf needs more data + delta = self.socket.recv(8096) + if delta == b'': + return None + self._buf += delta + msgs = self._parse_messages() + self._msgs.extend(msgs) + return self._msgs.popleft() + + def _send_closing_frame(self, ignore_send_errors=False): + """Sends the closing frame to the client, if required.""" + if self.version == 76 and not self.websocket_closed: + try: + self.socket.sendall(b"\xff\x00") + except OSError: + # Sometimes, like when the remote side cuts off the connection, + # we don't care about this. + if not ignore_send_errors: # pragma NO COVER + raise + self.websocket_closed = True + + def close(self): + """Forcibly close the websocket; generally it is preferable to + return from the handler method.""" + try: + self._send_closing_frame(True) + self.socket.shutdown(True) + except OSError as e: + if e.errno != errno.ENOTCONN: + self.log.write('{ctx} socket shutdown error: {e}'.format(ctx=self.log_context, e=e)) + finally: + self.socket.close() + + +class ConnectionClosedError(Exception): + pass + + +class FailedConnectionError(Exception): + def __init__(self, status, message): + super().__init__(status, message) + self.message = message + self.status = status + + +class ProtocolError(ValueError): + pass + + +class RFC6455WebSocket(WebSocket): + def __init__(self, sock, environ, version=13, protocol=None, client=False, extensions=None, + max_frame_length=DEFAULT_MAX_FRAME_LENGTH): + super().__init__(sock, environ, version) + self.iterator = self._iter_frames() + self.client = client + self.protocol = protocol + self.extensions = extensions or {} + + self._deflate_enc = None + self._deflate_dec = None + self.max_frame_length = max_frame_length + self._remote_close_data = None + + class UTF8Decoder: + def __init__(self): + if utf8validator: + self.validator = utf8validator.Utf8Validator() + else: + self.validator = None + decoderclass = codecs.getincrementaldecoder('utf8') + self.decoder = decoderclass() + + def reset(self): + if self.validator: + self.validator.reset() + self.decoder.reset() + + def decode(self, data, final=False): + if self.validator: + valid, eocp, c_i, t_i = self.validator.validate(data) + if not valid: + raise ValueError('Data is not valid unicode') + return self.decoder.decode(data, final) + + def _get_permessage_deflate_enc(self): + options = self.extensions.get("permessage-deflate") + if options is None: + return None + + def _make(): + return zlib.compressobj(zlib.Z_DEFAULT_COMPRESSION, zlib.DEFLATED, + -options.get("client_max_window_bits" if self.client + else "server_max_window_bits", + zlib.MAX_WBITS)) + + if options.get("client_no_context_takeover" if self.client + else "server_no_context_takeover"): + # This option means we have to make a new one every time + return _make() + else: + if self._deflate_enc is None: + self._deflate_enc = _make() + return self._deflate_enc + + def _get_permessage_deflate_dec(self, rsv1): + options = self.extensions.get("permessage-deflate") + if options is None or not rsv1: + return None + + def _make(): + return zlib.decompressobj(-options.get("server_max_window_bits" if self.client + else "client_max_window_bits", + zlib.MAX_WBITS)) + + if options.get("server_no_context_takeover" if self.client + else "client_no_context_takeover"): + # This option means we have to make a new one every time + return _make() + else: + if self._deflate_dec is None: + self._deflate_dec = _make() + return self._deflate_dec + + def _get_bytes(self, numbytes): + data = b'' + while len(data) < numbytes: + d = self.socket.recv(numbytes - len(data)) + if not d: + raise ConnectionClosedError() + data = data + d + return data + + class Message: + def __init__(self, opcode, max_frame_length, decoder=None, decompressor=None): + self.decoder = decoder + self.data = [] + self.finished = False + self.opcode = opcode + self.decompressor = decompressor + self.max_frame_length = max_frame_length + + def push(self, data, final=False): + self.finished = final + self.data.append(data) + + def getvalue(self): + data = b"".join(self.data) + if not self.opcode & 8 and self.decompressor: + data = self.decompressor.decompress(data + b"\x00\x00\xff\xff", self.max_frame_length) + if self.decompressor.unconsumed_tail: + raise FailedConnectionError( + 1009, + "Incoming compressed frame exceeds length limit of {} bytes.".format(self.max_frame_length)) + + if self.decoder: + data = self.decoder.decode(data, self.finished) + return data + + @staticmethod + def _apply_mask(data, mask, length=None, offset=0): + if length is None: + length = len(data) + cnt = range(length) + return b''.join(bytes((data[i] ^ mask[(offset + i) % 4],)) for i in cnt) + + def _handle_control_frame(self, opcode, data): + if opcode == 8: # connection close + self._remote_close_data = data + if not data: + status = 1000 + elif len(data) > 1: + status = struct.unpack_from('!H', data)[0] + if not status or status not in VALID_CLOSE_STATUS: + raise FailedConnectionError( + 1002, + "Unexpected close status code.") + try: + data = self.UTF8Decoder().decode(data[2:], True) + except (UnicodeDecodeError, ValueError): + raise FailedConnectionError( + 1002, + "Close message data should be valid UTF-8.") + else: + status = 1002 + self.close(close_data=(status, '')) + raise ConnectionClosedError() + elif opcode == 9: # ping + self.send(data, control_code=0xA) + elif opcode == 0xA: # pong + pass + else: + raise FailedConnectionError( + 1002, "Unknown control frame received.") + + def _iter_frames(self): + fragmented_message = None + try: + while True: + message = self._recv_frame(message=fragmented_message) + if message.opcode & 8: + self._handle_control_frame( + message.opcode, message.getvalue()) + continue + if fragmented_message and message is not fragmented_message: + raise RuntimeError('Unexpected message change.') + fragmented_message = message + if message.finished: + data = fragmented_message.getvalue() + fragmented_message = None + yield data + except FailedConnectionError: + exc_typ, exc_val, exc_tb = sys.exc_info() + self.close(close_data=(exc_val.status, exc_val.message)) + except ConnectionClosedError: + return + except Exception: + self.close(close_data=(1011, 'Internal Server Error')) + raise + + def _recv_frame(self, message=None): + recv = self._get_bytes + + # Unpacking the frame described in Section 5.2 of RFC6455 + # (https://tools.ietf.org/html/rfc6455#section-5.2) + header = recv(2) + a, b = struct.unpack('!BB', header) + finished = a >> 7 == 1 + rsv123 = a >> 4 & 7 + rsv1 = rsv123 & 4 + if rsv123: + if rsv1 and "permessage-deflate" not in self.extensions: + # must be zero - unless it's compressed then rsv1 is true + raise FailedConnectionError( + 1002, + "RSV1, RSV2, RSV3: MUST be 0 unless an extension is" + " negotiated that defines meanings for non-zero values.") + opcode = a & 15 + if opcode not in (0, 1, 2, 8, 9, 0xA): + raise FailedConnectionError(1002, "Unknown opcode received.") + masked = b & 128 == 128 + if not masked and not self.client: + raise FailedConnectionError(1002, "A client MUST mask all frames" + " that it sends to the server") + length = b & 127 + if opcode & 8: + if not finished: + raise FailedConnectionError(1002, "Control frames must not" + " be fragmented.") + if length > 125: + raise FailedConnectionError( + 1002, + "All control frames MUST have a payload length of 125" + " bytes or less") + elif opcode and message: + raise FailedConnectionError( + 1002, + "Received a non-continuation opcode within" + " fragmented message.") + elif not opcode and not message: + raise FailedConnectionError( + 1002, + "Received continuation opcode with no previous" + " fragments received.") + if length == 126: + length = struct.unpack('!H', recv(2))[0] + elif length == 127: + length = struct.unpack('!Q', recv(8))[0] + + if length > self.max_frame_length: + raise FailedConnectionError(1009, "Incoming frame of {} bytes is above length limit of {} bytes.".format( + length, self.max_frame_length)) + if masked: + mask = struct.unpack('!BBBB', recv(4)) + received = 0 + if not message or opcode & 8: + decoder = self.UTF8Decoder() if opcode == 1 else None + decompressor = self._get_permessage_deflate_dec(rsv1) + message = self.Message(opcode, self.max_frame_length, decoder=decoder, decompressor=decompressor) + if not length: + message.push(b'', final=finished) + else: + while received < length: + d = self.socket.recv(length - received) + if not d: + raise ConnectionClosedError() + dlen = len(d) + if masked: + d = self._apply_mask(d, mask, length=dlen, offset=received) + received = received + dlen + try: + message.push(d, final=finished) + except (UnicodeDecodeError, ValueError): + raise FailedConnectionError( + 1007, "Text data must be valid utf-8") + return message + + def _pack_message(self, message, masked=False, + continuation=False, final=True, control_code=None): + is_text = False + if isinstance(message, str): + message = message.encode('utf-8') + is_text = True + + compress_bit = 0 + compressor = self._get_permessage_deflate_enc() + # Control frames are identified by opcodes where the most significant + # bit of the opcode is 1. Currently defined opcodes for control frames + # include 0x8 (Close), 0x9 (Ping), and 0xA (Pong). Opcodes 0xB-0xF are + # reserved for further control frames yet to be defined. + # https://datatracker.ietf.org/doc/html/rfc6455#section-5.5 + is_control_frame = (control_code or 0) & 8 + # An endpoint MUST NOT set the "Per-Message Compressed" bit of control + # frames and non-first fragments of a data message. An endpoint + # receiving such a frame MUST _Fail the WebSocket Connection_. + # https://datatracker.ietf.org/doc/html/rfc7692#section-6.1 + if message and compressor and not is_control_frame: + message = compressor.compress(message) + message += compressor.flush(zlib.Z_SYNC_FLUSH) + assert message[-4:] == b"\x00\x00\xff\xff" + message = message[:-4] + compress_bit = 1 << 6 + + length = len(message) + if not length: + # no point masking empty data + masked = False + if control_code: + if control_code not in (8, 9, 0xA): + raise ProtocolError('Unknown control opcode.') + if continuation or not final: + raise ProtocolError('Control frame cannot be a fragment.') + if length > 125: + raise ProtocolError('Control frame data too large (>125).') + header = struct.pack('!B', control_code | 1 << 7) + else: + opcode = 0 if continuation else ((1 if is_text else 2) | compress_bit) + header = struct.pack('!B', opcode | (1 << 7 if final else 0)) + lengthdata = 1 << 7 if masked else 0 + if length > 65535: + lengthdata = struct.pack('!BQ', lengthdata | 127, length) + elif length > 125: + lengthdata = struct.pack('!BH', lengthdata | 126, length) + else: + lengthdata = struct.pack('!B', lengthdata | length) + if masked: + # NOTE: RFC6455 states: + # A server MUST NOT mask any frames that it sends to the client + rand = Random(time.time()) + mask = [rand.getrandbits(8) for _ in range(4)] + message = RFC6455WebSocket._apply_mask(message, mask, length) + maskdata = struct.pack('!BBBB', *mask) + else: + maskdata = b'' + + return b''.join((header, lengthdata, maskdata, message)) + + def wait(self): + for i in self.iterator: + return i + + def _send(self, frame): + self._sendlock.acquire() + try: + self.socket.sendall(frame) + finally: + self._sendlock.release() + + def send(self, message, **kw): + kw['masked'] = self.client + payload = self._pack_message(message, **kw) + self._send(payload) + + def _send_closing_frame(self, ignore_send_errors=False, close_data=None): + if self.version in (8, 13) and not self.websocket_closed: + if close_data is not None: + status, msg = close_data + if isinstance(msg, str): + msg = msg.encode('utf-8') + data = struct.pack('!H', status) + msg + else: + data = '' + try: + self.send(data, control_code=8) + except OSError: + # Sometimes, like when the remote side cuts off the connection, + # we don't care about this. + if not ignore_send_errors: # pragma NO COVER + raise + self.websocket_closed = True + + def close(self, close_data=None): + """Forcibly close the websocket; generally it is preferable to + return from the handler method.""" + try: + self._send_closing_frame(close_data=close_data, ignore_send_errors=True) + self.socket.shutdown(socket.SHUT_WR) + except OSError as e: + if e.errno != errno.ENOTCONN: + self.log.write('{ctx} socket shutdown error: {e}'.format(ctx=self.log_context, e=e)) + finally: + self.socket.close() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/wsgi.py b/netdeploy/lib/python3.11/site-packages/eventlet/wsgi.py new file mode 100644 index 0000000..b6b4d0c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/wsgi.py @@ -0,0 +1,1102 @@ +import errno +import os +import sys +import time +import traceback +import types +import urllib.parse +import warnings + +import eventlet +from eventlet import greenio +from eventlet import support +from eventlet.corolocal import local +from eventlet.green import BaseHTTPServer +from eventlet.green import socket + + +DEFAULT_MAX_SIMULTANEOUS_REQUESTS = 1024 +DEFAULT_MAX_HTTP_VERSION = 'HTTP/1.1' +MAX_REQUEST_LINE = 8192 +MAX_HEADER_LINE = 8192 +MAX_TOTAL_HEADER_SIZE = 65536 +MINIMUM_CHUNK_SIZE = 4096 +# %(client_port)s is also available +DEFAULT_LOG_FORMAT = ('%(client_ip)s - - [%(date_time)s] "%(request_line)s"' + ' %(status_code)s %(body_length)s %(wall_seconds).6f') +RESPONSE_414 = b'''HTTP/1.0 414 Request URI Too Long\r\n\ +Connection: close\r\n\ +Content-Length: 0\r\n\r\n''' +is_accepting = True + +STATE_IDLE = 'idle' +STATE_REQUEST = 'request' +STATE_CLOSE = 'close' + +__all__ = ['server', 'format_date_time'] + +# Weekday and month names for HTTP date/time formatting; always English! +_weekdayname = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"] +_monthname = [None, # Dummy so we can use 1-based month numbers + "Jan", "Feb", "Mar", "Apr", "May", "Jun", + "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + + +def format_date_time(timestamp): + """Formats a unix timestamp into an HTTP standard string.""" + year, month, day, hh, mm, ss, wd, _y, _z = time.gmtime(timestamp) + return "%s, %02d %3s %4d %02d:%02d:%02d GMT" % ( + _weekdayname[wd], day, _monthname[month], year, hh, mm, ss + ) + + +def addr_to_host_port(addr): + host = 'unix' + port = '' + if isinstance(addr, tuple): + host = addr[0] + port = addr[1] + return (host, port) + + +# Collections of error codes to compare against. Not all attributes are set +# on errno module on all platforms, so some are literals :( +BAD_SOCK = {errno.EBADF, 10053} +BROKEN_SOCK = {errno.EPIPE, errno.ECONNRESET, errno.ESHUTDOWN} + + +class ChunkReadError(ValueError): + pass + + +WSGI_LOCAL = local() + + +class Input: + + def __init__(self, + rfile, + content_length, + sock, + wfile=None, + wfile_line=None, + chunked_input=False): + + self.rfile = rfile + self._sock = sock + if content_length is not None: + content_length = int(content_length) + self.content_length = content_length + + self.wfile = wfile + self.wfile_line = wfile_line + + self.position = 0 + self.chunked_input = chunked_input + self.chunk_length = -1 + + # (optional) headers to send with a "100 Continue" response. Set by + # calling set_hundred_continue_respose_headers() on env['wsgi.input'] + self.hundred_continue_headers = None + self.is_hundred_continue_response_sent = False + + # handle_one_response should give us a ref to the response state so we + # know whether we can still send the 100 Continue; until then, though, + # we're flying blind + self.headers_sent = None + + def send_hundred_continue_response(self): + if self.headers_sent: + # To late; application has already started sending data back + # to the client + # TODO: maybe log a warning if self.hundred_continue_headers + # is not None? + return + + towrite = [] + + # 100 Continue status line + towrite.append(self.wfile_line) + + # Optional headers + if self.hundred_continue_headers is not None: + # 100 Continue headers + for header in self.hundred_continue_headers: + towrite.append(('%s: %s\r\n' % header).encode()) + + # Blank line + towrite.append(b'\r\n') + + self.wfile.writelines(towrite) + self.wfile.flush() + + # Reinitialize chunk_length (expect more data) + self.chunk_length = -1 + + @property + def should_send_hundred_continue(self): + return self.wfile is not None and not self.is_hundred_continue_response_sent + + def _do_read(self, reader, length=None): + if self.should_send_hundred_continue: + # 100 Continue response + self.send_hundred_continue_response() + self.is_hundred_continue_response_sent = True + if length is None or length > self.content_length - self.position: + length = self.content_length - self.position + if not length: + return b'' + try: + read = reader(length) + except greenio.SSL.ZeroReturnError: + read = b'' + self.position += len(read) + return read + + def _discard_trailers(self, rfile): + while True: + line = rfile.readline() + if not line or line in (b'\r\n', b'\n', b''): + break + + def _chunked_read(self, rfile, length=None, use_readline=False): + if self.should_send_hundred_continue: + # 100 Continue response + self.send_hundred_continue_response() + self.is_hundred_continue_response_sent = True + try: + if length == 0: + return b"" + + if length and length < 0: + length = None + + if use_readline: + reader = self.rfile.readline + else: + reader = self.rfile.read + + response = [] + while self.chunk_length != 0: + maxreadlen = self.chunk_length - self.position + if length is not None and length < maxreadlen: + maxreadlen = length + + if maxreadlen > 0: + data = reader(maxreadlen) + if not data: + self.chunk_length = 0 + raise OSError("unexpected end of file while parsing chunked data") + + datalen = len(data) + response.append(data) + + self.position += datalen + if self.chunk_length == self.position: + rfile.readline() + + if length is not None: + length -= datalen + if length == 0: + break + if use_readline and data[-1:] == b"\n": + break + else: + try: + self.chunk_length = int(rfile.readline().split(b";", 1)[0], 16) + except ValueError as err: + raise ChunkReadError(err) + self.position = 0 + if self.chunk_length == 0: + self._discard_trailers(rfile) + except greenio.SSL.ZeroReturnError: + pass + return b''.join(response) + + def read(self, length=None): + if self.chunked_input: + return self._chunked_read(self.rfile, length) + return self._do_read(self.rfile.read, length) + + def readline(self, size=None): + if self.chunked_input: + return self._chunked_read(self.rfile, size, True) + else: + return self._do_read(self.rfile.readline, size) + + def readlines(self, hint=None): + if self.chunked_input: + lines = [] + for line in iter(self.readline, b''): + lines.append(line) + if hint and hint > 0: + hint -= len(line) + if hint <= 0: + break + return lines + else: + return self._do_read(self.rfile.readlines, hint) + + def __iter__(self): + return iter(self.read, b'') + + def get_socket(self): + return self._sock + + def set_hundred_continue_response_headers(self, headers, + capitalize_response_headers=True): + # Response headers capitalization (default) + # CONTent-TYpe: TExt/PlaiN -> Content-Type: TExt/PlaiN + # Per HTTP RFC standard, header name is case-insensitive. + # Please, fix your client to ignore header case if possible. + if capitalize_response_headers: + headers = [ + ('-'.join([x.capitalize() for x in key.split('-')]), value) + for key, value in headers] + self.hundred_continue_headers = headers + + def discard(self, buffer_size=16 << 10): + while self.read(buffer_size): + pass + + +class HeaderLineTooLong(Exception): + pass + + +class HeadersTooLarge(Exception): + pass + + +def get_logger(log, debug): + if callable(getattr(log, 'info', None)) \ + and callable(getattr(log, 'debug', None)): + return log + else: + return LoggerFileWrapper(log or sys.stderr, debug) + + +class LoggerNull: + def __init__(self): + pass + + def error(self, msg, *args, **kwargs): + pass + + def info(self, msg, *args, **kwargs): + pass + + def debug(self, msg, *args, **kwargs): + pass + + def write(self, msg, *args): + pass + + +class LoggerFileWrapper(LoggerNull): + def __init__(self, log, debug): + self.log = log + self._debug = debug + + def error(self, msg, *args, **kwargs): + self.write(msg, *args) + + def info(self, msg, *args, **kwargs): + self.write(msg, *args) + + def debug(self, msg, *args, **kwargs): + if self._debug: + self.write(msg, *args) + + def write(self, msg, *args): + msg = msg + '\n' + if args: + msg = msg % args + self.log.write(msg) + + +class FileObjectForHeaders: + + def __init__(self, fp): + self.fp = fp + self.total_header_size = 0 + + def readline(self, size=-1): + sz = size + if size < 0: + sz = MAX_HEADER_LINE + rv = self.fp.readline(sz) + if len(rv) >= MAX_HEADER_LINE: + raise HeaderLineTooLong() + self.total_header_size += len(rv) + if self.total_header_size > MAX_TOTAL_HEADER_SIZE: + raise HeadersTooLarge() + return rv + + +class HttpProtocol(BaseHTTPServer.BaseHTTPRequestHandler): + """This class is used to handle the HTTP requests that arrive + at the server. + + The handler will parse the request and the headers, then call a method + specific to the request type. + + :param conn_state: The given connection status. + :param server: The server accessible by the request handler. + """ + protocol_version = 'HTTP/1.1' + minimum_chunk_size = MINIMUM_CHUNK_SIZE + capitalize_response_headers = True + reject_bad_requests = True + + # https://github.com/eventlet/eventlet/issues/295 + # Stdlib default is 0 (unbuffered), but then `wfile.writelines()` looses data + # so before going back to unbuffered, remove any usage of `writelines`. + wbufsize = 16 << 10 + + def __init__(self, conn_state, server): + self.request = conn_state[1] + self.client_address = conn_state[0] + self.conn_state = conn_state + self.server = server + # Want to allow some overrides from the server before running setup + if server.minimum_chunk_size is not None: + self.minimum_chunk_size = server.minimum_chunk_size + self.capitalize_response_headers = server.capitalize_response_headers + + self.setup() + try: + self.handle() + finally: + self.finish() + + def setup(self): + # overriding SocketServer.setup to correctly handle SSL.Connection objects + conn = self.connection = self.request + + # TCP_QUICKACK is a better alternative to disabling Nagle's algorithm + # https://news.ycombinator.com/item?id=10607422 + if getattr(socket, 'TCP_QUICKACK', None): + try: + conn.setsockopt(socket.IPPROTO_TCP, socket.TCP_QUICKACK, True) + except OSError: + pass + + try: + self.rfile = conn.makefile('rb', self.rbufsize) + self.wfile = conn.makefile('wb', self.wbufsize) + except (AttributeError, NotImplementedError): + if hasattr(conn, 'send') and hasattr(conn, 'recv'): + # it's an SSL.Connection + self.rfile = socket._fileobject(conn, "rb", self.rbufsize) + self.wfile = socket._fileobject(conn, "wb", self.wbufsize) + else: + # it's a SSLObject, or a martian + raise NotImplementedError( + '''eventlet.wsgi doesn't support sockets of type {}'''.format(type(conn))) + + def handle(self): + self.close_connection = True + + while True: + self.handle_one_request() + if self.conn_state[2] == STATE_CLOSE: + self.close_connection = 1 + else: + self.conn_state[2] = STATE_IDLE + if self.close_connection: + break + + def _read_request_line(self): + if self.rfile.closed: + self.close_connection = 1 + return '' + + try: + sock = self.connection + if self.server.keepalive and not isinstance(self.server.keepalive, bool): + sock.settimeout(self.server.keepalive) + line = self.rfile.readline(self.server.url_length_limit) + sock.settimeout(self.server.socket_timeout) + return line + except greenio.SSL.ZeroReturnError: + pass + except OSError as e: + last_errno = support.get_errno(e) + if last_errno in BROKEN_SOCK: + self.server.log.debug('({}) connection reset by peer {!r}'.format( + self.server.pid, + self.client_address)) + elif last_errno not in BAD_SOCK: + raise + return '' + + def handle_one_request(self): + if self.server.max_http_version: + self.protocol_version = self.server.max_http_version + + self.raw_requestline = self._read_request_line() + self.conn_state[2] = STATE_REQUEST + if not self.raw_requestline: + self.close_connection = 1 + return + if len(self.raw_requestline) >= self.server.url_length_limit: + self.wfile.write(RESPONSE_414) + self.close_connection = 1 + return + + orig_rfile = self.rfile + try: + self.rfile = FileObjectForHeaders(self.rfile) + if not self.parse_request(): + return + except HeaderLineTooLong: + self.wfile.write( + b"HTTP/1.0 400 Header Line Too Long\r\n" + b"Connection: close\r\nContent-length: 0\r\n\r\n") + self.close_connection = 1 + return + except HeadersTooLarge: + self.wfile.write( + b"HTTP/1.0 400 Headers Too Large\r\n" + b"Connection: close\r\nContent-length: 0\r\n\r\n") + self.close_connection = 1 + return + finally: + self.rfile = orig_rfile + + content_length = self.headers.get('content-length') + transfer_encoding = self.headers.get('transfer-encoding') + if content_length is not None: + try: + if int(content_length) < 0: + raise ValueError + except ValueError: + # Negative, or not an int at all + self.wfile.write( + b"HTTP/1.0 400 Bad Request\r\n" + b"Connection: close\r\nContent-length: 0\r\n\r\n") + self.close_connection = 1 + return + + if transfer_encoding is not None: + if self.reject_bad_requests: + msg = b"Content-Length and Transfer-Encoding are not allowed together\n" + self.wfile.write( + b"HTTP/1.0 400 Bad Request\r\n" + b"Connection: close\r\n" + b"Content-Length: %d\r\n" + b"\r\n%s" % (len(msg), msg)) + self.close_connection = 1 + return + + self.environ = self.get_environ() + self.application = self.server.app + try: + self.server.outstanding_requests += 1 + try: + self.handle_one_response() + except OSError as e: + # Broken pipe, connection reset by peer + if support.get_errno(e) not in BROKEN_SOCK: + raise + finally: + self.server.outstanding_requests -= 1 + + def handle_one_response(self): + start = time.time() + headers_set = [] + headers_sent = [] + # Grab the request input now; app may try to replace it in the environ + request_input = self.environ['eventlet.input'] + # Push the headers-sent state into the Input so it won't send a + # 100 Continue response if we've already started a response. + request_input.headers_sent = headers_sent + + wfile = self.wfile + result = None + use_chunked = [False] + length = [0] + status_code = [200] + # Status code of 1xx or 204 or 2xx to CONNECT request MUST NOT send body and related headers + # https://httpwg.org/specs/rfc7230.html#rfc.section.3.3.1 + bodyless = [False] + + def write(data): + towrite = [] + if not headers_set: + raise AssertionError("write() before start_response()") + elif not headers_sent: + status, response_headers = headers_set + headers_sent.append(1) + header_list = [header[0].lower() for header in response_headers] + towrite.append(('%s %s\r\n' % (self.protocol_version, status)).encode()) + for header in response_headers: + towrite.append(('%s: %s\r\n' % header).encode('latin-1')) + + # send Date header? + if 'date' not in header_list: + towrite.append(('Date: %s\r\n' % (format_date_time(time.time()),)).encode()) + + client_conn = self.headers.get('Connection', '').lower() + send_keep_alive = False + if self.close_connection == 0 and \ + self.server.keepalive and (client_conn == 'keep-alive' or + (self.request_version == 'HTTP/1.1' and + not client_conn == 'close')): + # only send keep-alives back to clients that sent them, + # it's redundant for 1.1 connections + send_keep_alive = (client_conn == 'keep-alive') + self.close_connection = 0 + else: + self.close_connection = 1 + + if 'content-length' not in header_list: + if bodyless[0]: + pass # client didn't expect a body anyway + elif self.request_version == 'HTTP/1.1': + use_chunked[0] = True + towrite.append(b'Transfer-Encoding: chunked\r\n') + else: + # client is 1.0 and therefore must read to EOF + self.close_connection = 1 + + if self.close_connection: + towrite.append(b'Connection: close\r\n') + elif send_keep_alive: + towrite.append(b'Connection: keep-alive\r\n') + # Spec says timeout must be an integer, but we allow sub-second + int_timeout = int(self.server.keepalive or 0) + if not isinstance(self.server.keepalive, bool) and int_timeout: + towrite.append(b'Keep-Alive: timeout=%d\r\n' % int_timeout) + towrite.append(b'\r\n') + # end of header writing + + if use_chunked[0]: + # Write the chunked encoding + towrite.append(("%x" % (len(data),)).encode() + b"\r\n" + data + b"\r\n") + else: + towrite.append(data) + wfile.writelines(towrite) + wfile.flush() + length[0] = length[0] + sum(map(len, towrite)) + + def start_response(status, response_headers, exc_info=None): + status_code[0] = int(status.split(" ", 1)[0]) + if exc_info: + try: + if headers_sent: + # Re-raise original exception if headers sent + raise exc_info[1].with_traceback(exc_info[2]) + finally: + # Avoid dangling circular ref + exc_info = None + + bodyless[0] = ( + status_code[0] in (204, 304) + or self.command == "HEAD" + or (100 <= status_code[0] < 200) + or (self.command == "CONNECT" and 200 <= status_code[0] < 300) + ) + + # Response headers capitalization + # CONTent-TYpe: TExt/PlaiN -> Content-Type: TExt/PlaiN + # Per HTTP RFC standard, header name is case-insensitive. + # Please, fix your client to ignore header case if possible. + if self.capitalize_response_headers: + def cap(x): + return x.encode('latin1').capitalize().decode('latin1') + + response_headers = [ + ('-'.join([cap(x) for x in key.split('-')]), value) + for key, value in response_headers] + + headers_set[:] = [status, response_headers] + return write + + try: + try: + WSGI_LOCAL.already_handled = False + result = self.application(self.environ, start_response) + + # Set content-length if possible + if headers_set and not headers_sent and hasattr(result, '__len__'): + # We've got a complete final response + if not bodyless[0] and 'Content-Length' not in [h for h, _v in headers_set[1]]: + headers_set[1].append(('Content-Length', str(sum(map(len, result))))) + if request_input.should_send_hundred_continue: + # We've got a complete final response, and never sent a 100 Continue. + # There's no chance we'll need to read the body as we stream out the + # response, so we can be nice and send a Connection: close header. + self.close_connection = 1 + + towrite = [] + towrite_size = 0 + just_written_size = 0 + minimum_write_chunk_size = int(self.environ.get( + 'eventlet.minimum_write_chunk_size', self.minimum_chunk_size)) + for data in result: + if len(data) == 0: + continue + if isinstance(data, str): + data = data.encode('ascii') + + towrite.append(data) + towrite_size += len(data) + if towrite_size >= minimum_write_chunk_size: + write(b''.join(towrite)) + towrite = [] + just_written_size = towrite_size + towrite_size = 0 + if WSGI_LOCAL.already_handled: + self.close_connection = 1 + return + if towrite: + just_written_size = towrite_size + write(b''.join(towrite)) + if not headers_sent or (use_chunked[0] and just_written_size): + write(b'') + except (Exception, eventlet.Timeout): + self.close_connection = 1 + tb = traceback.format_exc() + self.server.log.info(tb) + if not headers_sent: + err_body = tb.encode() if self.server.debug else b'' + start_response("500 Internal Server Error", + [('Content-type', 'text/plain'), + ('Content-length', len(err_body))]) + write(err_body) + finally: + if hasattr(result, 'close'): + result.close() + if request_input.should_send_hundred_continue: + # We just sent the final response, no 100 Continue. Client may or + # may not have started to send a body, and if we keep the connection + # open we've seen clients either + # * send a body, then start a new request + # * skip the body and go straight to a new request + # Looks like the most broadly compatible option is to close the + # connection and let the client retry. + # https://curl.se/mail/lib-2004-08/0002.html + # Note that we likely *won't* send a Connection: close header at this point + self.close_connection = 1 + + if (request_input.chunked_input or + request_input.position < (request_input.content_length or 0)): + # Read and discard body if connection is going to be reused + if self.close_connection == 0: + try: + request_input.discard() + except ChunkReadError as e: + self.close_connection = 1 + self.server.log.error(( + 'chunked encoding error while discarding request body.' + + ' client={0} request="{1}" error="{2}"').format( + self.get_client_address()[0], self.requestline, e, + )) + except OSError as e: + self.close_connection = 1 + self.server.log.error(( + 'I/O error while discarding request body.' + + ' client={0} request="{1}" error="{2}"').format( + self.get_client_address()[0], self.requestline, e, + )) + finish = time.time() + + for hook, args, kwargs in self.environ['eventlet.posthooks']: + hook(self.environ, *args, **kwargs) + + if self.server.log_output: + client_host, client_port = self.get_client_address() + + self.server.log.info(self.server.log_format % { + 'client_ip': client_host, + 'client_port': client_port, + 'date_time': self.log_date_time_string(), + 'request_line': self.requestline, + 'status_code': status_code[0], + 'body_length': length[0], + 'wall_seconds': finish - start, + }) + + def get_client_address(self): + host, port = addr_to_host_port(self.client_address) + + if self.server.log_x_forwarded_for: + forward = self.headers.get('X-Forwarded-For', '').replace(' ', '') + if forward: + host = forward + ',' + host + return (host, port) + + def formalize_key_naming(self, k): + """ + Headers containing underscores are permitted by RFC9110, + but evenlet joining headers of different names into + the same environment variable will dangerously confuse applications as to which is which. + Cf. + - Nginx: http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers + - Django: https://www.djangoproject.com/weblog/2015/jan/13/security/ + - Gunicorn: https://github.com/benoitc/gunicorn/commit/72b8970dbf2bf3444eb2e8b12aeff1a3d5922a9a + - Werkzeug: https://github.com/pallets/werkzeug/commit/5ee439a692dc4474e0311de2496b567eed2d02cf + - ... + """ + if "_" in k: + return + + return k.replace('-', '_').upper() + + def get_environ(self): + env = self.server.get_environ() + env['REQUEST_METHOD'] = self.command + env['SCRIPT_NAME'] = '' + + pq = self.path.split('?', 1) + env['RAW_PATH_INFO'] = pq[0] + env['PATH_INFO'] = urllib.parse.unquote(pq[0], encoding='latin1') + if len(pq) > 1: + env['QUERY_STRING'] = pq[1] + + ct = self.headers.get('content-type') + if ct is None: + try: + ct = self.headers.type + except AttributeError: + ct = self.headers.get_content_type() + env['CONTENT_TYPE'] = ct + + length = self.headers.get('content-length') + if length: + env['CONTENT_LENGTH'] = length + env['SERVER_PROTOCOL'] = 'HTTP/1.0' + + sockname = self.request.getsockname() + server_addr = addr_to_host_port(sockname) + env['SERVER_NAME'] = server_addr[0] + env['SERVER_PORT'] = str(server_addr[1]) + client_addr = addr_to_host_port(self.client_address) + env['REMOTE_ADDR'] = client_addr[0] + env['REMOTE_PORT'] = str(client_addr[1]) + env['GATEWAY_INTERFACE'] = 'CGI/1.1' + + try: + headers = self.headers.headers + except AttributeError: + headers = self.headers._headers + else: + headers = [h.split(':', 1) for h in headers] + + env['headers_raw'] = headers_raw = tuple((k, v.strip(' \t\n\r')) for k, v in headers) + for k, v in headers_raw: + k = self.formalize_key_naming(k) + if not k: + continue + + if k in ('CONTENT_TYPE', 'CONTENT_LENGTH'): + # These do not get the HTTP_ prefix and were handled above + continue + envk = 'HTTP_' + k + if envk in env: + env[envk] += ',' + v + else: + env[envk] = v + + if env.get('HTTP_EXPECT', '').lower() == '100-continue': + wfile = self.wfile + wfile_line = b'HTTP/1.1 100 Continue\r\n' + else: + wfile = None + wfile_line = None + chunked = env.get('HTTP_TRANSFER_ENCODING', '').lower() == 'chunked' + if not chunked and length is None: + # https://www.rfc-editor.org/rfc/rfc9112#section-6.3-2.7 + # "If this is a request message and none of the above are true, then + # the message body length is zero (no message body is present)." + length = '0' + env['wsgi.input'] = env['eventlet.input'] = Input( + self.rfile, length, self.connection, wfile=wfile, wfile_line=wfile_line, + chunked_input=chunked) + env['eventlet.posthooks'] = [] + + # WebSocketWSGI needs a way to flag the connection as idle, + # since it may never fall out of handle_one_request + def set_idle(): + self.conn_state[2] = STATE_IDLE + env['eventlet.set_idle'] = set_idle + + return env + + def finish(self): + try: + BaseHTTPServer.BaseHTTPRequestHandler.finish(self) + except OSError as e: + # Broken pipe, connection reset by peer + if support.get_errno(e) not in BROKEN_SOCK: + raise + greenio.shutdown_safe(self.connection) + self.connection.close() + + def handle_expect_100(self): + return True + + +class Server(BaseHTTPServer.HTTPServer): + + def __init__(self, + socket, + address, + app, + log=None, + environ=None, + max_http_version=None, + protocol=HttpProtocol, + minimum_chunk_size=None, + log_x_forwarded_for=True, + keepalive=True, + log_output=True, + log_format=DEFAULT_LOG_FORMAT, + url_length_limit=MAX_REQUEST_LINE, + debug=True, + socket_timeout=None, + capitalize_response_headers=True): + + self.outstanding_requests = 0 + self.socket = socket + self.address = address + self.log = LoggerNull() + if log_output: + self.log = get_logger(log, debug) + self.app = app + self.keepalive = keepalive + self.environ = environ + self.max_http_version = max_http_version + self.protocol = protocol + self.pid = os.getpid() + self.minimum_chunk_size = minimum_chunk_size + self.log_x_forwarded_for = log_x_forwarded_for + self.log_output = log_output + self.log_format = log_format + self.url_length_limit = url_length_limit + self.debug = debug + self.socket_timeout = socket_timeout + self.capitalize_response_headers = capitalize_response_headers + + if not self.capitalize_response_headers: + warnings.warn("""capitalize_response_headers is disabled. + Please, make sure you know what you are doing. + HTTP headers names are case-insensitive per RFC standard. + Most likely, you need to fix HTTP parsing in your client software.""", + DeprecationWarning, stacklevel=3) + + def get_environ(self): + d = { + 'wsgi.errors': sys.stderr, + 'wsgi.version': (1, 0), + 'wsgi.multithread': True, + 'wsgi.multiprocess': False, + 'wsgi.run_once': False, + 'wsgi.url_scheme': 'http', + } + # detect secure socket + if hasattr(self.socket, 'do_handshake'): + d['wsgi.url_scheme'] = 'https' + d['HTTPS'] = 'on' + if self.environ is not None: + d.update(self.environ) + return d + + def process_request(self, conn_state): + try: + # protocol is responsible for pulling out any overrides it needs itself + # before it starts processing + self.protocol(conn_state, self) + except socket.timeout: + # Expected exceptions are not exceptional + conn_state[1].close() + # similar to logging "accepted" in server() + self.log.debug('({}) timed out {!r}'.format(self.pid, conn_state[0])) + + def log_message(self, message): + raise AttributeError('''\ +eventlet.wsgi.server.log_message was deprecated and deleted. +Please use server.log.info instead.''') + + +try: + import ssl + ACCEPT_EXCEPTIONS = (socket.error, ssl.SSLError) + ACCEPT_ERRNO = {errno.EPIPE, errno.ECONNRESET, + errno.ESHUTDOWN, ssl.SSL_ERROR_EOF, ssl.SSL_ERROR_SSL} +except ImportError: + ACCEPT_EXCEPTIONS = (socket.error,) + ACCEPT_ERRNO = {errno.EPIPE, errno.ECONNRESET, errno.ESHUTDOWN} + + +def socket_repr(sock): + scheme = 'http' + if hasattr(sock, 'do_handshake'): + scheme = 'https' + + name = sock.getsockname() + if sock.family == socket.AF_INET: + hier_part = '//{}:{}'.format(*name) + elif sock.family == socket.AF_INET6: + hier_part = '//[{}]:{}'.format(*name[:2]) + elif sock.family == socket.AF_UNIX: + hier_part = name + else: + hier_part = repr(name) + + return scheme + ':' + hier_part + + +def server(sock, site, + log=None, + environ=None, + max_size=None, + max_http_version=DEFAULT_MAX_HTTP_VERSION, + protocol=HttpProtocol, + server_event=None, + minimum_chunk_size=None, + log_x_forwarded_for=True, + custom_pool=None, + keepalive=True, + log_output=True, + log_format=DEFAULT_LOG_FORMAT, + url_length_limit=MAX_REQUEST_LINE, + debug=True, + socket_timeout=None, + capitalize_response_headers=True): + """Start up a WSGI server handling requests from the supplied server + socket. This function loops forever. The *sock* object will be + closed after server exits, but the underlying file descriptor will + remain open, so if you have a dup() of *sock*, it will remain usable. + + .. warning:: + + At the moment :func:`server` will always wait for active connections to finish before + exiting, even if there's an exception raised inside it + (*all* exceptions are handled the same way, including :class:`greenlet.GreenletExit` + and those inheriting from `BaseException`). + + While this may not be an issue normally, when it comes to long running HTTP connections + (like :mod:`eventlet.websocket`) it will become problematic and calling + :meth:`~eventlet.greenthread.GreenThread.wait` on a thread that runs the server may hang, + even after using :meth:`~eventlet.greenthread.GreenThread.kill`, as long + as there are active connections. + + :param sock: Server socket, must be already bound to a port and listening. + :param site: WSGI application function. + :param log: logging.Logger instance or file-like object that logs should be written to. + If a Logger instance is supplied, messages are sent to the INFO log level. + If not specified, sys.stderr is used. + :param environ: Additional parameters that go into the environ dictionary of every request. + :param max_size: Maximum number of client connections opened at any time by this server. + Default is 1024. + :param max_http_version: Set to "HTTP/1.0" to make the server pretend it only supports HTTP 1.0. + This can help with applications or clients that don't behave properly using HTTP 1.1. + :param protocol: Protocol class. Deprecated. + :param server_event: Used to collect the Server object. Deprecated. + :param minimum_chunk_size: Minimum size in bytes for http chunks. This can be used to improve + performance of applications which yield many small strings, though + using it technically violates the WSGI spec. This can be overridden + on a per request basis by setting environ['eventlet.minimum_write_chunk_size']. + :param log_x_forwarded_for: If True (the default), logs the contents of the x-forwarded-for + header in addition to the actual client ip address in the 'client_ip' field of the + log line. + :param custom_pool: A custom GreenPool instance which is used to spawn client green threads. + If this is supplied, max_size is ignored. + :param keepalive: If set to False or zero, disables keepalives on the server; all connections + will be closed after serving one request. If numeric, it will be the timeout used + when reading the next request. + :param log_output: A Boolean indicating if the server will log data or not. + :param log_format: A python format string that is used as the template to generate log lines. + The following values can be formatted into it: client_ip, date_time, request_line, + status_code, body_length, wall_seconds. The default is a good example of how to + use it. + :param url_length_limit: A maximum allowed length of the request url. If exceeded, 414 error + is returned. + :param debug: True if the server should send exception tracebacks to the clients on 500 errors. + If False, the server will respond with empty bodies. + :param socket_timeout: Timeout for client connections' socket operations. Default None means + wait forever. + :param capitalize_response_headers: Normalize response headers' names to Foo-Bar. + Default is True. + """ + serv = Server( + sock, sock.getsockname(), + site, log, + environ=environ, + max_http_version=max_http_version, + protocol=protocol, + minimum_chunk_size=minimum_chunk_size, + log_x_forwarded_for=log_x_forwarded_for, + keepalive=keepalive, + log_output=log_output, + log_format=log_format, + url_length_limit=url_length_limit, + debug=debug, + socket_timeout=socket_timeout, + capitalize_response_headers=capitalize_response_headers, + ) + if server_event is not None: + warnings.warn( + 'eventlet.wsgi.Server() server_event kwarg is deprecated and will be removed soon', + DeprecationWarning, stacklevel=2) + server_event.send(serv) + if max_size is None: + max_size = DEFAULT_MAX_SIMULTANEOUS_REQUESTS + if custom_pool is not None: + pool = custom_pool + else: + pool = eventlet.GreenPool(max_size) + + if not (hasattr(pool, 'spawn') and hasattr(pool, 'waitall')): + raise AttributeError('''\ +eventlet.wsgi.Server pool must provide methods: `spawn`, `waitall`. +If unsure, use eventlet.GreenPool.''') + + # [addr, socket, state] + connections = {} + + def _clean_connection(_, conn): + connections.pop(conn[0], None) + conn[2] = STATE_CLOSE + greenio.shutdown_safe(conn[1]) + conn[1].close() + + try: + serv.log.info('({}) wsgi starting up on {}'.format(serv.pid, socket_repr(sock))) + while is_accepting: + try: + client_socket, client_addr = sock.accept() + client_socket.settimeout(serv.socket_timeout) + serv.log.debug('({}) accepted {!r}'.format(serv.pid, client_addr)) + connections[client_addr] = connection = [client_addr, client_socket, STATE_IDLE] + (pool.spawn(serv.process_request, connection) + .link(_clean_connection, connection)) + except ACCEPT_EXCEPTIONS as e: + if support.get_errno(e) not in ACCEPT_ERRNO: + raise + else: + break + except (KeyboardInterrupt, SystemExit): + serv.log.info('wsgi exiting') + break + finally: + for cs in connections.values(): + prev_state = cs[2] + cs[2] = STATE_CLOSE + if prev_state == STATE_IDLE: + greenio.shutdown_safe(cs[1]) + pool.waitall() + serv.log.info('({}) wsgi exited, is_accepting={}'.format(serv.pid, is_accepting)) + try: + # NOTE: It's not clear whether we want this to leave the + # socket open or close it. Use cases like Spawning want + # the underlying fd to remain open, but if we're going + # that far we might as well not bother closing sock at + # all. + sock.close() + except OSError as e: + if support.get_errno(e) not in BROKEN_SOCK: + traceback.print_exc() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/README.rst b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/README.rst new file mode 100644 index 0000000..b094781 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/README.rst @@ -0,0 +1,130 @@ +eventlet.zipkin +=============== + +`Zipkin `_ is a distributed tracing system developed at Twitter. +This package provides a WSGI application using eventlet +with tracing facility that complies with Zipkin. + +Why use it? +From the http://twitter.github.io/zipkin/: + +"Collecting traces helps developers gain deeper knowledge about how +certain requests perform in a distributed system. Let's say we're having +problems with user requests timing out. We can look up traced requests +that timed out and display it in the web UI. We'll be able to quickly +find the service responsible for adding the unexpected response time. If +the service has been annotated adequately we can also find out where in +that service the issue is happening." + + +Screenshot +---------- + +Zipkin web ui screenshots obtained when applying this module to +`OpenStack swift `_ are in example/. + + +Requirement +----------- + +A eventlet.zipkin needs `python scribe client `_ +and `thrift `_ (>=0.9), +because the zipkin collector speaks `scribe `_ protocol. +Below command will install both scribe client and thrift. + +Install facebook-scribe: + +:: + + pip install facebook-scribe + +**Python**: ``2.7`` (Because the current Python Thrift release doesn't support Python 3) + + +How to use +---------- + +Add tracing facility to your application +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +Apply the monkey patch before you start wsgi server. + +.. code:: python + + # Add only 2 lines to your code + from eventlet.zipkin import patcher + patcher.enable_trace_patch() + + # existing code + from eventlet import wsgi + wsgi.server(sock, app) + +You can pass some parameters to ``enable_trace_patch()`` + +* host: Scribe daemon IP address (default: '127.0.0.1') +* port: Scribe daemon port (default: 9410) +* trace_app_log: A Boolean indicating if the tracer will trace application log together or not. This facility assume that your application uses python standard logging library. (default: False) +* sampling_rate: A Float value (0.0~1.0) that indicates the tracing frequency. If you specify 1.0, all requests are traced and sent to Zipkin collecotr. If you specify 0.1, only 1/10 requests are traced. (defult: 1.0) + + +(Option) Annotation API +~~~~~~~~~~~~~~~~~~~~~~~ +If you want to record additional information, +you can use below API from anywhere in your code. + +.. code:: python + + from eventlet.zipkin import api + + api.put_annotation('Cache miss for %s' % request) + api.put_key_value('key', 'value') + + + + +Zipkin simple setup +------------------- + +:: + + $ git clone https://github.com/twitter/zipkin.git + $ cd zipkin + # Open 3 terminals + (terminal1) $ bin/collector + (terminal2) $ bin/query + (terminal3) $ bin/web + +Access http://localhost:8080 from your browser. + + +(Option) fluentd +---------------- +If you want to buffer the tracing data for performance, +`fluentd scribe plugin `_ is available. +Since ``out_scribe plugin`` extends `Buffer Plugin `_ , +you can customize buffering parameters in the manner of fluentd. +Scribe plugin is included in td-agent by default. + + +Sample: ``/etc/td-agent/td-agent.conf`` + +:: + + # in_scribe + + type scribe + port 9999 + + + # out_scribe + + type scribe + host Zipkin_collector_IP + port 9410 + flush_interval 60s + buffer_chunk_limit 256m + + +| And, you need to specify ``patcher.enable_trace_patch(port=9999)`` for in_scribe. +| In this case, trace data is passed like below. +| Your application => Local fluentd in_scribe (9999) => Local fluentd out_scribe =====> Remote zipkin collector (9410) + diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/README.rst b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/README.rst new file mode 100644 index 0000000..0317d50 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/README.rst @@ -0,0 +1,8 @@ +_thrift +======== + +* This directory is auto-generated by Thrift Compiler by using + https://github.com/twitter/zipkin/blob/master/zipkin-thrift/src/main/thrift/com/twitter/zipkin/zipkinCore.thrift + +* Do not modify this directory. + diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore.thrift b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore.thrift new file mode 100644 index 0000000..0787ca8 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore.thrift @@ -0,0 +1,55 @@ +# Copyright 2012 Twitter Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +namespace java com.twitter.zipkin.gen +namespace rb Zipkin + +//************** Collection related structs ************** + +// these are the annotations we always expect to find in a span +const string CLIENT_SEND = "cs" +const string CLIENT_RECV = "cr" +const string SERVER_SEND = "ss" +const string SERVER_RECV = "sr" + +// this represents a host and port in a network +struct Endpoint { + 1: i32 ipv4, + 2: i16 port // beware that this will give us negative ports. some conversion needed + 3: string service_name // which service did this operation happen on? +} + +// some event took place, either one by the framework or by the user +struct Annotation { + 1: i64 timestamp // microseconds from epoch + 2: string value // what happened at the timestamp? + 3: optional Endpoint host // host this happened on +} + +enum AnnotationType { BOOL, BYTES, I16, I32, I64, DOUBLE, STRING } + +struct BinaryAnnotation { + 1: string key, + 2: binary value, + 3: AnnotationType annotation_type, + 4: optional Endpoint host +} + +struct Span { + 1: i64 trace_id // unique trace id, use for all spans in trace + 3: string name, // span name, rpc method for example + 4: i64 id, // unique span id, only used for this span + 5: optional i64 parent_id, // parent span id + 6: list annotations, // list of all annotations/events that occured + 8: list binary_annotations // any binary annotations +} diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/__init__.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/__init__.py new file mode 100644 index 0000000..adefd8e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/__init__.py @@ -0,0 +1 @@ +__all__ = ['ttypes', 'constants'] diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/constants.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/constants.py new file mode 100644 index 0000000..3e04f77 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/constants.py @@ -0,0 +1,14 @@ +# +# Autogenerated by Thrift Compiler (0.8.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# + +from thrift.Thrift import TType, TMessageType, TException +from ttypes import * + +CLIENT_SEND = "cs" +CLIENT_RECV = "cr" +SERVER_SEND = "ss" +SERVER_RECV = "sr" diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/ttypes.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/ttypes.py new file mode 100644 index 0000000..418911f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/ttypes.py @@ -0,0 +1,452 @@ +# +# Autogenerated by Thrift Compiler (0.8.0) +# +# DO NOT EDIT UNLESS YOU ARE SURE THAT YOU KNOW WHAT YOU ARE DOING +# +# + +from thrift.Thrift import TType, TMessageType, TException + +from thrift.transport import TTransport +from thrift.protocol import TBinaryProtocol, TProtocol +try: + from thrift.protocol import fastbinary +except: + fastbinary = None + + +class AnnotationType: + BOOL = 0 + BYTES = 1 + I16 = 2 + I32 = 3 + I64 = 4 + DOUBLE = 5 + STRING = 6 + + _VALUES_TO_NAMES = { + 0: "BOOL", + 1: "BYTES", + 2: "I16", + 3: "I32", + 4: "I64", + 5: "DOUBLE", + 6: "STRING", + } + + _NAMES_TO_VALUES = { + "BOOL": 0, + "BYTES": 1, + "I16": 2, + "I32": 3, + "I64": 4, + "DOUBLE": 5, + "STRING": 6, + } + + +class Endpoint: + """ + Attributes: + - ipv4 + - port + - service_name + """ + + thrift_spec = ( + None, # 0 + (1, TType.I32, 'ipv4', None, None, ), # 1 + (2, TType.I16, 'port', None, None, ), # 2 + (3, TType.STRING, 'service_name', None, None, ), # 3 + ) + + def __init__(self, ipv4=None, port=None, service_name=None,): + self.ipv4 = ipv4 + self.port = port + self.service_name = service_name + + def read(self, iprot): + if iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None and fastbinary is not None: + fastbinary.decode_binary(self, iprot.trans, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I32: + self.ipv4 = iprot.readI32(); + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.I16: + self.port = iprot.readI16(); + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.STRING: + self.service_name = iprot.readString(); + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and self.thrift_spec is not None and fastbinary is not None: + oprot.trans.write(fastbinary.encode_binary(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Endpoint') + if self.ipv4 is not None: + oprot.writeFieldBegin('ipv4', TType.I32, 1) + oprot.writeI32(self.ipv4) + oprot.writeFieldEnd() + if self.port is not None: + oprot.writeFieldBegin('port', TType.I16, 2) + oprot.writeI16(self.port) + oprot.writeFieldEnd() + if self.service_name is not None: + oprot.writeFieldBegin('service_name', TType.STRING, 3) + oprot.writeString(self.service_name) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.iteritems()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + +class Annotation: + """ + Attributes: + - timestamp + - value + - host + """ + + thrift_spec = ( + None, # 0 + (1, TType.I64, 'timestamp', None, None, ), # 1 + (2, TType.STRING, 'value', None, None, ), # 2 + (3, TType.STRUCT, 'host', (Endpoint, Endpoint.thrift_spec), None, ), # 3 + ) + + def __init__(self, timestamp=None, value=None, host=None,): + self.timestamp = timestamp + self.value = value + self.host = host + + def read(self, iprot): + if iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None and fastbinary is not None: + fastbinary.decode_binary(self, iprot.trans, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I64: + self.timestamp = iprot.readI64(); + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.STRING: + self.value = iprot.readString(); + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.STRUCT: + self.host = Endpoint() + self.host.read(iprot) + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and self.thrift_spec is not None and fastbinary is not None: + oprot.trans.write(fastbinary.encode_binary(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Annotation') + if self.timestamp is not None: + oprot.writeFieldBegin('timestamp', TType.I64, 1) + oprot.writeI64(self.timestamp) + oprot.writeFieldEnd() + if self.value is not None: + oprot.writeFieldBegin('value', TType.STRING, 2) + oprot.writeString(self.value) + oprot.writeFieldEnd() + if self.host is not None: + oprot.writeFieldBegin('host', TType.STRUCT, 3) + self.host.write(oprot) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.iteritems()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + +class BinaryAnnotation: + """ + Attributes: + - key + - value + - annotation_type + - host + """ + + thrift_spec = ( + None, # 0 + (1, TType.STRING, 'key', None, None, ), # 1 + (2, TType.STRING, 'value', None, None, ), # 2 + (3, TType.I32, 'annotation_type', None, None, ), # 3 + (4, TType.STRUCT, 'host', (Endpoint, Endpoint.thrift_spec), None, ), # 4 + ) + + def __init__(self, key=None, value=None, annotation_type=None, host=None,): + self.key = key + self.value = value + self.annotation_type = annotation_type + self.host = host + + def read(self, iprot): + if iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None and fastbinary is not None: + fastbinary.decode_binary(self, iprot.trans, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.STRING: + self.key = iprot.readString(); + else: + iprot.skip(ftype) + elif fid == 2: + if ftype == TType.STRING: + self.value = iprot.readString(); + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.I32: + self.annotation_type = iprot.readI32(); + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.STRUCT: + self.host = Endpoint() + self.host.read(iprot) + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and self.thrift_spec is not None and fastbinary is not None: + oprot.trans.write(fastbinary.encode_binary(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('BinaryAnnotation') + if self.key is not None: + oprot.writeFieldBegin('key', TType.STRING, 1) + oprot.writeString(self.key) + oprot.writeFieldEnd() + if self.value is not None: + oprot.writeFieldBegin('value', TType.STRING, 2) + oprot.writeString(self.value) + oprot.writeFieldEnd() + if self.annotation_type is not None: + oprot.writeFieldBegin('annotation_type', TType.I32, 3) + oprot.writeI32(self.annotation_type) + oprot.writeFieldEnd() + if self.host is not None: + oprot.writeFieldBegin('host', TType.STRUCT, 4) + self.host.write(oprot) + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.iteritems()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) + +class Span: + """ + Attributes: + - trace_id + - name + - id + - parent_id + - annotations + - binary_annotations + """ + + thrift_spec = ( + None, # 0 + (1, TType.I64, 'trace_id', None, None, ), # 1 + None, # 2 + (3, TType.STRING, 'name', None, None, ), # 3 + (4, TType.I64, 'id', None, None, ), # 4 + (5, TType.I64, 'parent_id', None, None, ), # 5 + (6, TType.LIST, 'annotations', (TType.STRUCT,(Annotation, Annotation.thrift_spec)), None, ), # 6 + None, # 7 + (8, TType.LIST, 'binary_annotations', (TType.STRUCT,(BinaryAnnotation, BinaryAnnotation.thrift_spec)), None, ), # 8 + ) + + def __init__(self, trace_id=None, name=None, id=None, parent_id=None, annotations=None, binary_annotations=None,): + self.trace_id = trace_id + self.name = name + self.id = id + self.parent_id = parent_id + self.annotations = annotations + self.binary_annotations = binary_annotations + + def read(self, iprot): + if iprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and isinstance(iprot.trans, TTransport.CReadableTransport) and self.thrift_spec is not None and fastbinary is not None: + fastbinary.decode_binary(self, iprot.trans, (self.__class__, self.thrift_spec)) + return + iprot.readStructBegin() + while True: + (fname, ftype, fid) = iprot.readFieldBegin() + if ftype == TType.STOP: + break + if fid == 1: + if ftype == TType.I64: + self.trace_id = iprot.readI64(); + else: + iprot.skip(ftype) + elif fid == 3: + if ftype == TType.STRING: + self.name = iprot.readString(); + else: + iprot.skip(ftype) + elif fid == 4: + if ftype == TType.I64: + self.id = iprot.readI64(); + else: + iprot.skip(ftype) + elif fid == 5: + if ftype == TType.I64: + self.parent_id = iprot.readI64(); + else: + iprot.skip(ftype) + elif fid == 6: + if ftype == TType.LIST: + self.annotations = [] + (_etype3, _size0) = iprot.readListBegin() + for _i4 in xrange(_size0): + _elem5 = Annotation() + _elem5.read(iprot) + self.annotations.append(_elem5) + iprot.readListEnd() + else: + iprot.skip(ftype) + elif fid == 8: + if ftype == TType.LIST: + self.binary_annotations = [] + (_etype9, _size6) = iprot.readListBegin() + for _i10 in xrange(_size6): + _elem11 = BinaryAnnotation() + _elem11.read(iprot) + self.binary_annotations.append(_elem11) + iprot.readListEnd() + else: + iprot.skip(ftype) + else: + iprot.skip(ftype) + iprot.readFieldEnd() + iprot.readStructEnd() + + def write(self, oprot): + if oprot.__class__ == TBinaryProtocol.TBinaryProtocolAccelerated and self.thrift_spec is not None and fastbinary is not None: + oprot.trans.write(fastbinary.encode_binary(self, (self.__class__, self.thrift_spec))) + return + oprot.writeStructBegin('Span') + if self.trace_id is not None: + oprot.writeFieldBegin('trace_id', TType.I64, 1) + oprot.writeI64(self.trace_id) + oprot.writeFieldEnd() + if self.name is not None: + oprot.writeFieldBegin('name', TType.STRING, 3) + oprot.writeString(self.name) + oprot.writeFieldEnd() + if self.id is not None: + oprot.writeFieldBegin('id', TType.I64, 4) + oprot.writeI64(self.id) + oprot.writeFieldEnd() + if self.parent_id is not None: + oprot.writeFieldBegin('parent_id', TType.I64, 5) + oprot.writeI64(self.parent_id) + oprot.writeFieldEnd() + if self.annotations is not None: + oprot.writeFieldBegin('annotations', TType.LIST, 6) + oprot.writeListBegin(TType.STRUCT, len(self.annotations)) + for iter12 in self.annotations: + iter12.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + if self.binary_annotations is not None: + oprot.writeFieldBegin('binary_annotations', TType.LIST, 8) + oprot.writeListBegin(TType.STRUCT, len(self.binary_annotations)) + for iter13 in self.binary_annotations: + iter13.write(oprot) + oprot.writeListEnd() + oprot.writeFieldEnd() + oprot.writeFieldStop() + oprot.writeStructEnd() + + def validate(self): + return + + + def __repr__(self): + L = ['%s=%r' % (key, value) + for key, value in self.__dict__.iteritems()] + return '%s(%s)' % (self.__class__.__name__, ', '.join(L)) + + def __eq__(self, other): + return isinstance(other, self.__class__) and self.__dict__ == other.__dict__ + + def __ne__(self, other): + return not (self == other) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/api.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/api.py new file mode 100644 index 0000000..8edde5c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/api.py @@ -0,0 +1,187 @@ +import os +import sys +import time +import struct +import socket +import random + +from eventlet.green import threading +from eventlet.zipkin._thrift.zipkinCore import ttypes +from eventlet.zipkin._thrift.zipkinCore.constants import SERVER_SEND + + +client = None +_tls = threading.local() # thread local storage + + +def put_annotation(msg, endpoint=None): + """ This is annotation API. + You can add your own annotation from in your code. + Annotation is recorded with timestamp automatically. + e.g.) put_annotation('cache hit for %s' % request) + + :param msg: String message + :param endpoint: host info + """ + if is_sample(): + a = ZipkinDataBuilder.build_annotation(msg, endpoint) + trace_data = get_trace_data() + trace_data.add_annotation(a) + + +def put_key_value(key, value, endpoint=None): + """ This is binary annotation API. + You can add your own key-value extra information from in your code. + Key-value doesn't have a time component. + e.g.) put_key_value('http.uri', '/hoge/index.html') + + :param key: String + :param value: String + :param endpoint: host info + """ + if is_sample(): + b = ZipkinDataBuilder.build_binary_annotation(key, value, endpoint) + trace_data = get_trace_data() + trace_data.add_binary_annotation(b) + + +def is_tracing(): + """ Return whether the current thread is tracking or not """ + return hasattr(_tls, 'trace_data') + + +def is_sample(): + """ Return whether it should record trace information + for the request or not + """ + return is_tracing() and _tls.trace_data.sampled + + +def get_trace_data(): + if is_tracing(): + return _tls.trace_data + + +def set_trace_data(trace_data): + _tls.trace_data = trace_data + + +def init_trace_data(): + if is_tracing(): + del _tls.trace_data + + +def _uniq_id(): + """ + Create a random 64-bit signed integer appropriate + for use as trace and span IDs. + XXX: By experimentation zipkin has trouble recording traces with ids + larger than (2 ** 56) - 1 + """ + return random.randint(0, (2 ** 56) - 1) + + +def generate_trace_id(): + return _uniq_id() + + +def generate_span_id(): + return _uniq_id() + + +class TraceData: + + END_ANNOTATION = SERVER_SEND + + def __init__(self, name, trace_id, span_id, parent_id, sampled, endpoint): + """ + :param name: RPC name (String) + :param trace_id: int + :param span_id: int + :param parent_id: int or None + :param sampled: lets the downstream servers know + if I should record trace data for the request (bool) + :param endpoint: zipkin._thrift.zipkinCore.ttypes.EndPoint + """ + self.name = name + self.trace_id = trace_id + self.span_id = span_id + self.parent_id = parent_id + self.sampled = sampled + self.endpoint = endpoint + self.annotations = [] + self.bannotations = [] + self._done = False + + def add_annotation(self, annotation): + if annotation.host is None: + annotation.host = self.endpoint + if not self._done: + self.annotations.append(annotation) + if annotation.value == self.END_ANNOTATION: + self.flush() + + def add_binary_annotation(self, bannotation): + if bannotation.host is None: + bannotation.host = self.endpoint + if not self._done: + self.bannotations.append(bannotation) + + def flush(self): + span = ZipkinDataBuilder.build_span(name=self.name, + trace_id=self.trace_id, + span_id=self.span_id, + parent_id=self.parent_id, + annotations=self.annotations, + bannotations=self.bannotations) + client.send_to_collector(span) + self.annotations = [] + self.bannotations = [] + self._done = True + + +class ZipkinDataBuilder: + @staticmethod + def build_span(name, trace_id, span_id, parent_id, + annotations, bannotations): + return ttypes.Span( + name=name, + trace_id=trace_id, + id=span_id, + parent_id=parent_id, + annotations=annotations, + binary_annotations=bannotations + ) + + @staticmethod + def build_annotation(value, endpoint=None): + if isinstance(value, str): + value = value.encode('utf-8') + assert isinstance(value, bytes) + return ttypes.Annotation(time.time() * 1000 * 1000, + value, endpoint) + + @staticmethod + def build_binary_annotation(key, value, endpoint=None): + annotation_type = ttypes.AnnotationType.STRING + return ttypes.BinaryAnnotation(key, value, annotation_type, endpoint) + + @staticmethod + def build_endpoint(ipv4=None, port=None, service_name=None): + if ipv4 is not None: + ipv4 = ZipkinDataBuilder._ipv4_to_int(ipv4) + if service_name is None: + service_name = ZipkinDataBuilder._get_script_name() + return ttypes.Endpoint( + ipv4=ipv4, + port=port, + service_name=service_name + ) + + @staticmethod + def _ipv4_to_int(ipv4): + return struct.unpack('!i', socket.inet_aton(ipv4))[0] + + @staticmethod + def _get_script_name(): + return os.path.basename(sys.argv[0]) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/client.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/client.py new file mode 100644 index 0000000..faff244 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/client.py @@ -0,0 +1,56 @@ +import base64 +import warnings + +from scribe import scribe +from thrift.transport import TTransport, TSocket +from thrift.protocol import TBinaryProtocol + +from eventlet import GreenPile + + +CATEGORY = 'zipkin' + + +class ZipkinClient: + + def __init__(self, host='127.0.0.1', port=9410): + """ + :param host: zipkin collector IP address (default '127.0.0.1') + :param port: zipkin collector port (default 9410) + """ + self.host = host + self.port = port + self.pile = GreenPile(1) + self._connect() + + def _connect(self): + socket = TSocket.TSocket(self.host, self.port) + self.transport = TTransport.TFramedTransport(socket) + protocol = TBinaryProtocol.TBinaryProtocol(self.transport, + False, False) + self.scribe_client = scribe.Client(protocol) + try: + self.transport.open() + except TTransport.TTransportException as e: + warnings.warn(e.message) + + def _build_message(self, thrift_obj): + trans = TTransport.TMemoryBuffer() + protocol = TBinaryProtocol.TBinaryProtocolAccelerated(trans=trans) + thrift_obj.write(protocol) + return base64.b64encode(trans.getvalue()) + + def send_to_collector(self, span): + self.pile.spawn(self._send, span) + + def _send(self, span): + log_entry = scribe.LogEntry(CATEGORY, self._build_message(span)) + try: + self.scribe_client.Log([log_entry]) + except Exception as e: + msg = 'ZipkinClient send error %s' % str(e) + warnings.warn(msg) + self._connect() + + def close(self): + self.transport.close() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/example/ex1.png b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/example/ex1.png new file mode 100755 index 0000000000000000000000000000000000000000..7f7a0497e4266e593414a8a3dba43c1ab42722fd GIT binary patch literal 53179 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%0ATU6V5Xpc`?$tvQA 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�M*!eShP<2D7c7#o0Kb(!I=n(% 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%0DD?r|8pyZ?3f%x<=Xv_p(DCy{li$ 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 literal 0 HcmV?d00001 diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/example/ex3.png b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/example/ex3.png new file mode 100755 index 0000000000000000000000000000000000000000..5ff98603e359ac3733a551d45f1da2ffc55c5c9b GIT binary patch literal 73175 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 M-v4FAzY>%G14MZ?HUIzs literal 0 HcmV?d00001 diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/greenthread.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/greenthread.py new file mode 100644 index 0000000..37e12d6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/greenthread.py @@ -0,0 +1,33 @@ +from eventlet import greenthread + +from eventlet.zipkin import api + + +__original_init__ = greenthread.GreenThread.__init__ +__original_main__ = greenthread.GreenThread.main + + +def _patched__init(self, parent): + # parent thread saves current TraceData from tls to self + if api.is_tracing(): + self.trace_data = api.get_trace_data() + + __original_init__(self, parent) + + +def _patched_main(self, function, args, kwargs): + # child thread inherits TraceData + if hasattr(self, 'trace_data'): + api.set_trace_data(self.trace_data) + + __original_main__(self, function, args, kwargs) + + +def patch(): + greenthread.GreenThread.__init__ = _patched__init + greenthread.GreenThread.main = _patched_main + + +def unpatch(): + greenthread.GreenThread.__init__ = __original_init__ + greenthread.GreenThread.main = __original_main__ diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/http.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/http.py new file mode 100644 index 0000000..f981a17 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/http.py @@ -0,0 +1,29 @@ +import warnings + +from eventlet.green import httplib +from eventlet.zipkin import api + + +# see https://twitter.github.io/zipkin/Instrumenting.html +HDR_TRACE_ID = 'X-B3-TraceId' +HDR_SPAN_ID = 'X-B3-SpanId' +HDR_PARENT_SPAN_ID = 'X-B3-ParentSpanId' +HDR_SAMPLED = 'X-B3-Sampled' + + +def patch(): + warnings.warn("Since current Python thrift release \ + doesn't support Python 3, eventlet.zipkin.http \ + doesn't also support Python 3 (http.client)") + + +def unpatch(): + pass + + +def hex_str(n): + """ + Thrift uses a binary representation of trace and span ids + HTTP headers use a hexadecimal representation of the same + """ + return '%0.16x' % (n,) diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/log.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/log.py new file mode 100644 index 0000000..b7f9d32 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/log.py @@ -0,0 +1,19 @@ +import logging + +from eventlet.zipkin import api + + +__original_handle__ = logging.Logger.handle + + +def _patched_handle(self, record): + __original_handle__(self, record) + api.put_annotation(record.getMessage()) + + +def patch(): + logging.Logger.handle = _patched_handle + + +def unpatch(): + logging.Logger.handle = __original_handle__ diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/patcher.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/patcher.py new file mode 100644 index 0000000..8e7d8ad --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/patcher.py @@ -0,0 +1,41 @@ +from eventlet.zipkin import http +from eventlet.zipkin import wsgi +from eventlet.zipkin import greenthread +from eventlet.zipkin import log +from eventlet.zipkin import api +from eventlet.zipkin.client import ZipkinClient + + +def enable_trace_patch(host='127.0.0.1', port=9410, + trace_app_log=False, sampling_rate=1.0): + """ Apply monkey patch to trace your WSGI application. + + :param host: Scribe daemon IP address (default: '127.0.0.1') + :param port: Scribe daemon port (default: 9410) + :param trace_app_log: A Boolean indicating if the tracer will trace + application log together or not. This facility assume that + your application uses python standard logging library. + (default: False) + :param sampling_rate: A Float value (0.0~1.0) that indicates + the tracing frequency. If you specify 1.0, all request + are traced (and sent to Zipkin collecotr). + If you specify 0.1, only 1/10 requests are traced. (default: 1.0) + """ + api.client = ZipkinClient(host, port) + + # monkey patch for adding tracing facility + wsgi.patch(sampling_rate) + http.patch() + greenthread.patch() + + # monkey patch for capturing application log + if trace_app_log: + log.patch() + + +def disable_trace_patch(): + http.unpatch() + wsgi.unpatch() + greenthread.unpatch() + log.unpatch() + api.client.close() diff --git a/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/wsgi.py b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/wsgi.py new file mode 100644 index 0000000..402d142 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/eventlet/zipkin/wsgi.py @@ -0,0 +1,78 @@ +import random + +from eventlet import wsgi +from eventlet.zipkin import api +from eventlet.zipkin._thrift.zipkinCore.constants import \ + SERVER_RECV, SERVER_SEND +from eventlet.zipkin.http import \ + HDR_TRACE_ID, HDR_SPAN_ID, HDR_PARENT_SPAN_ID, HDR_SAMPLED + + +_sampler = None +__original_handle_one_response__ = wsgi.HttpProtocol.handle_one_response + + +def _patched_handle_one_response(self): + api.init_trace_data() + trace_id = int_or_none(self.headers.getheader(HDR_TRACE_ID)) + span_id = int_or_none(self.headers.getheader(HDR_SPAN_ID)) + parent_id = int_or_none(self.headers.getheader(HDR_PARENT_SPAN_ID)) + sampled = bool_or_none(self.headers.getheader(HDR_SAMPLED)) + if trace_id is None: # front-end server + trace_id = span_id = api.generate_trace_id() + parent_id = None + sampled = _sampler.sampling() + ip, port = self.request.getsockname()[:2] + ep = api.ZipkinDataBuilder.build_endpoint(ip, port) + trace_data = api.TraceData(name=self.command, + trace_id=trace_id, + span_id=span_id, + parent_id=parent_id, + sampled=sampled, + endpoint=ep) + api.set_trace_data(trace_data) + api.put_annotation(SERVER_RECV) + api.put_key_value('http.uri', self.path) + + __original_handle_one_response__(self) + + if api.is_sample(): + api.put_annotation(SERVER_SEND) + + +class Sampler: + def __init__(self, sampling_rate): + self.sampling_rate = sampling_rate + + def sampling(self): + # avoid generating unneeded random numbers + if self.sampling_rate == 1.0: + return True + r = random.random() + if r < self.sampling_rate: + return True + return False + + +def int_or_none(val): + if val is None: + return None + return int(val, 16) + + +def bool_or_none(val): + if val == '1': + return True + if val == '0': + return False + return None + + +def patch(sampling_rate): + global _sampler + _sampler = Sampler(sampling_rate) + wsgi.HttpProtocol.handle_one_response = _patched_handle_one_response + + +def unpatch(): + wsgi.HttpProtocol.handle_one_response = __original_handle_one_response__ diff --git a/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/INSTALLER b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/LICENSE.txt b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/LICENSE.txt new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2010 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/METADATA b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/METADATA new file mode 100644 index 0000000..5a02107 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/METADATA @@ -0,0 +1,101 @@ +Metadata-Version: 2.1 +Name: Flask +Version: 3.0.3 +Summary: A simple framework for building complex web applications. +Maintainer-email: Pallets +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Framework :: Flask +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Application +Classifier: Topic :: Software Development :: Libraries :: Application Frameworks +Classifier: Typing :: Typed +Requires-Dist: Werkzeug>=3.0.0 +Requires-Dist: Jinja2>=3.1.2 +Requires-Dist: itsdangerous>=2.1.2 +Requires-Dist: click>=8.1.3 +Requires-Dist: blinker>=1.6.2 +Requires-Dist: importlib-metadata>=3.6.0; python_version < '3.10' +Requires-Dist: asgiref>=3.2 ; extra == "async" +Requires-Dist: python-dotenv ; extra == "dotenv" +Project-URL: Changes, https://flask.palletsprojects.com/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://flask.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/flask/ +Provides-Extra: async +Provides-Extra: dotenv + +# Flask + +Flask is a lightweight [WSGI][] web application framework. It is designed +to make getting started quick and easy, with the ability to scale up to +complex applications. It began as a simple wrapper around [Werkzeug][] +and [Jinja][], and has become one of the most popular Python web +application frameworks. + +Flask offers suggestions, but doesn't enforce any dependencies or +project layout. It is up to the developer to choose the tools and +libraries they want to use. There are many extensions provided by the +community that make adding new functionality easy. + +[WSGI]: https://wsgi.readthedocs.io/ +[Werkzeug]: https://werkzeug.palletsprojects.com/ +[Jinja]: https://jinja.palletsprojects.com/ + + +## Installing + +Install and update from [PyPI][] using an installer such as [pip][]: + +``` +$ pip install -U Flask +``` + +[PyPI]: https://pypi.org/project/Flask/ +[pip]: https://pip.pypa.io/en/stable/getting-started/ + + +## A Simple Example + +```python +# save this as app.py +from flask import Flask + +app = Flask(__name__) + +@app.route("/") +def hello(): + return "Hello, World!" +``` + +``` +$ flask run + * Running on http://127.0.0.1:5000/ (Press CTRL+C to quit) +``` + + +## Contributing + +For guidance on setting up a development environment and how to make a +contribution to Flask, see the [contributing guidelines][]. + +[contributing guidelines]: https://github.com/pallets/flask/blob/main/CONTRIBUTING.rst + + +## Donate + +The Pallets organization develops and supports Flask and the libraries +it uses. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today][]. + +[please donate today]: https://palletsprojects.com/donate + diff --git a/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/RECORD b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/RECORD new file mode 100644 index 0000000..30c88c6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/RECORD @@ -0,0 +1,58 @@ +../../../bin/flask,sha256=yhZohlZaEYdJ73YLcFVixUBRsugqd4hMF_YJlNwgqMA,232 +flask-3.0.3.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +flask-3.0.3.dist-info/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +flask-3.0.3.dist-info/METADATA,sha256=exPahy4aahjV-mYqd9qb5HNP8haB_IxTuaotoSvCtag,3177 +flask-3.0.3.dist-info/RECORD,, +flask-3.0.3.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask-3.0.3.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 +flask-3.0.3.dist-info/entry_points.txt,sha256=bBP7hTOS5fz9zLtC7sPofBZAlMkEvBxu7KqS6l5lvc4,40 +flask/__init__.py,sha256=6xMqdVA0FIQ2U1KVaGX3lzNCdXPzoHPaa0hvQCNcfSk,2625 +flask/__main__.py,sha256=bYt9eEaoRQWdejEHFD8REx9jxVEdZptECFsV7F49Ink,30 +flask/__pycache__/__init__.cpython-311.pyc,, +flask/__pycache__/__main__.cpython-311.pyc,, +flask/__pycache__/app.cpython-311.pyc,, +flask/__pycache__/blueprints.cpython-311.pyc,, +flask/__pycache__/cli.cpython-311.pyc,, +flask/__pycache__/config.cpython-311.pyc,, +flask/__pycache__/ctx.cpython-311.pyc,, +flask/__pycache__/debughelpers.cpython-311.pyc,, +flask/__pycache__/globals.cpython-311.pyc,, +flask/__pycache__/helpers.cpython-311.pyc,, +flask/__pycache__/logging.cpython-311.pyc,, +flask/__pycache__/sessions.cpython-311.pyc,, +flask/__pycache__/signals.cpython-311.pyc,, +flask/__pycache__/templating.cpython-311.pyc,, +flask/__pycache__/testing.cpython-311.pyc,, +flask/__pycache__/typing.cpython-311.pyc,, +flask/__pycache__/views.cpython-311.pyc,, +flask/__pycache__/wrappers.cpython-311.pyc,, +flask/app.py,sha256=7-lh6cIj27riTE1Q18Ok1p5nOZ8qYiMux4Btc6o6mNc,60143 +flask/blueprints.py,sha256=7INXPwTkUxfOQXOOv1yu52NpHPmPGI5fMTMFZ-BG9yY,4430 +flask/cli.py,sha256=OOaf_Efqih1i2in58j-5ZZZmQnPpaSfiUFbEjlL9bzw,35825 +flask/config.py,sha256=bLzLVAj-cq-Xotu9erqOFte0xSFaVXyfz0AkP4GbwmY,13312 +flask/ctx.py,sha256=4atDhJJ_cpV1VMq4qsfU4E_61M1oN93jlS2H9gjrl58,15120 +flask/debughelpers.py,sha256=PGIDhStW_efRjpaa3zHIpo-htStJOR41Ip3OJWPYBwo,6080 +flask/globals.py,sha256=XdQZmStBmPIs8t93tjx6pO7Bm3gobAaONWkFcUHaGas,1713 +flask/helpers.py,sha256=tYrcQ_73GuSZVEgwFr-eMmV69UriFQDBmt8wZJIAqvg,23084 +flask/json/__init__.py,sha256=hLNR898paqoefdeAhraa5wyJy-bmRB2k2dV4EgVy2Z8,5602 +flask/json/__pycache__/__init__.cpython-311.pyc,, +flask/json/__pycache__/provider.cpython-311.pyc,, +flask/json/__pycache__/tag.cpython-311.pyc,, +flask/json/provider.py,sha256=q6iB83lSiopy80DZPrU-9mGcWwrD0mvLjiv9fHrRZgc,7646 +flask/json/tag.py,sha256=DhaNwuIOhdt2R74oOC9Y4Z8ZprxFYiRb5dUP5byyINw,9281 +flask/logging.py,sha256=8sM3WMTubi1cBb2c_lPkWpN0J8dMAqrgKRYLLi1dCVI,2377 +flask/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask/sansio/README.md,sha256=-0X1tECnilmz1cogx-YhNw5d7guK7GKrq_DEV2OzlU0,228 +flask/sansio/__pycache__/app.cpython-311.pyc,, +flask/sansio/__pycache__/blueprints.cpython-311.pyc,, +flask/sansio/__pycache__/scaffold.cpython-311.pyc,, +flask/sansio/app.py,sha256=YG5Gf7JVf1c0yccWDZ86q5VSfJUidOVp27HFxFNxC7U,38053 +flask/sansio/blueprints.py,sha256=Tqe-7EkZ-tbWchm8iDoCfD848f0_3nLv6NNjeIPvHwM,24637 +flask/sansio/scaffold.py,sha256=WLV9TRQMMhGlXz-1OKtQ3lv6mtIBQZxdW2HezYrGxoI,30633 +flask/sessions.py,sha256=RU4lzm9MQW9CtH8rVLRTDm8USMJyT4LbvYe7sxM2__k,14807 +flask/signals.py,sha256=V7lMUww7CqgJ2ThUBn1PiatZtQanOyt7OZpu2GZI-34,750 +flask/templating.py,sha256=2TcXLT85Asflm2W9WOSFxKCmYn5e49w_Jkg9-NaaJWo,7537 +flask/testing.py,sha256=3BFXb3bP7R5r-XLBuobhczbxDu8-1LWRzYuhbr-lwaE,10163 +flask/typing.py,sha256=ZavK-wV28Yv8CQB7u73qZp_jLalpbWdrXS37QR1ftN0,3190 +flask/views.py,sha256=B66bTvYBBcHMYk4dA1ScZD0oTRTBl0I5smp1lRm9riI,6939 +flask/wrappers.py,sha256=m1j5tIJxIu8_sPPgTAB_G4TTh52Q-HoDuw_qHV5J59g,5831 diff --git a/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/REQUESTED b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/WHEEL b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/WHEEL new file mode 100644 index 0000000..3b5e64b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.9.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/entry_points.txt b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/entry_points.txt new file mode 100644 index 0000000..eec6733 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask-3.0.3.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +flask=flask.cli:main + diff --git a/netdeploy/lib/python3.11/site-packages/flask/__init__.py b/netdeploy/lib/python3.11/site-packages/flask/__init__.py new file mode 100644 index 0000000..e86eb43 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/__init__.py @@ -0,0 +1,60 @@ +from __future__ import annotations + +import typing as t + +from . import json as json +from .app import Flask as Flask +from .blueprints import Blueprint as Blueprint +from .config import Config as Config +from .ctx import after_this_request as after_this_request +from .ctx import copy_current_request_context as copy_current_request_context +from .ctx import has_app_context as has_app_context +from .ctx import has_request_context as has_request_context +from .globals import current_app as current_app +from .globals import g as g +from .globals import request as request +from .globals import session as session +from .helpers import abort as abort +from .helpers import flash as flash +from .helpers import get_flashed_messages as get_flashed_messages +from .helpers import get_template_attribute as get_template_attribute +from .helpers import make_response as make_response +from .helpers import redirect as redirect +from .helpers import send_file as send_file +from .helpers import send_from_directory as send_from_directory +from .helpers import stream_with_context as stream_with_context +from .helpers import url_for as url_for +from .json import jsonify as jsonify +from .signals import appcontext_popped as appcontext_popped +from .signals import appcontext_pushed as appcontext_pushed +from .signals import appcontext_tearing_down as appcontext_tearing_down +from .signals import before_render_template as before_render_template +from .signals import got_request_exception as got_request_exception +from .signals import message_flashed as message_flashed +from .signals import request_finished as request_finished +from .signals import request_started as request_started +from .signals import request_tearing_down as request_tearing_down +from .signals import template_rendered as template_rendered +from .templating import render_template as render_template +from .templating import render_template_string as render_template_string +from .templating import stream_template as stream_template +from .templating import stream_template_string as stream_template_string +from .wrappers import Request as Request +from .wrappers import Response as Response + + +def __getattr__(name: str) -> t.Any: + if name == "__version__": + import importlib.metadata + import warnings + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " Flask 3.1. Use feature detection or" + " 'importlib.metadata.version(\"flask\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("flask") + + raise AttributeError(name) diff --git a/netdeploy/lib/python3.11/site-packages/flask/__main__.py b/netdeploy/lib/python3.11/site-packages/flask/__main__.py new file mode 100644 index 0000000..4e28416 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +main() diff --git a/netdeploy/lib/python3.11/site-packages/flask/app.py b/netdeploy/lib/python3.11/site-packages/flask/app.py new file mode 100644 index 0000000..7622b5e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/app.py @@ -0,0 +1,1498 @@ +from __future__ import annotations + +import collections.abc as cabc +import os +import sys +import typing as t +import weakref +from datetime import timedelta +from inspect import iscoroutinefunction +from itertools import chain +from types import TracebackType +from urllib.parse import quote as _url_quote + +import click +from werkzeug.datastructures import Headers +from werkzeug.datastructures import ImmutableDict +from werkzeug.exceptions import BadRequestKeyError +from werkzeug.exceptions import HTTPException +from werkzeug.exceptions import InternalServerError +from werkzeug.routing import BuildError +from werkzeug.routing import MapAdapter +from werkzeug.routing import RequestRedirect +from werkzeug.routing import RoutingException +from werkzeug.routing import Rule +from werkzeug.serving import is_running_from_reloader +from werkzeug.wrappers import Response as BaseResponse + +from . import cli +from . import typing as ft +from .ctx import AppContext +from .ctx import RequestContext +from .globals import _cv_app +from .globals import _cv_request +from .globals import current_app +from .globals import g +from .globals import request +from .globals import request_ctx +from .globals import session +from .helpers import get_debug_flag +from .helpers import get_flashed_messages +from .helpers import get_load_dotenv +from .helpers import send_from_directory +from .sansio.app import App +from .sansio.scaffold import _sentinel +from .sessions import SecureCookieSessionInterface +from .sessions import SessionInterface +from .signals import appcontext_tearing_down +from .signals import got_request_exception +from .signals import request_finished +from .signals import request_started +from .signals import request_tearing_down +from .templating import Environment +from .wrappers import Request +from .wrappers import Response + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIEnvironment + + from .testing import FlaskClient + from .testing import FlaskCliRunner + +T_shell_context_processor = t.TypeVar( + "T_shell_context_processor", bound=ft.ShellContextProcessorCallable +) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) + + +def _make_timedelta(value: timedelta | int | None) -> timedelta | None: + if value is None or isinstance(value, timedelta): + return value + + return timedelta(seconds=value) + + +class Flask(App): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the :file:`__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea of what + belongs to your application. This name is used to find resources + on the filesystem, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in :file:`yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplication.app` and not + `yourapplication.views.frontend`) + + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. + + .. versionadded:: 0.11 + The `root_path` parameter was added. + + .. versionadded:: 1.0 + The ``host_matching`` and ``static_host`` parameters were added. + + .. versionadded:: 1.0 + The ``subdomain_matching`` parameter was added. Subdomain + matching needs to be enabled manually now. Setting + :data:`SERVER_NAME` does not implicitly enable it. + + :param import_name: the name of the application package + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: The folder with static files that is served at + ``static_url_path``. Relative to the application ``root_path`` + or an absolute path. Defaults to ``'static'``. + :param static_host: the host to use when adding the static route. + Defaults to None. Required when using ``host_matching=True`` + with a ``static_folder`` configured. + :param host_matching: set ``url_map.host_matching`` attribute. + Defaults to False. + :param subdomain_matching: consider the subdomain relative to + :data:`SERVER_NAME` when matching routes. Defaults to False. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to ``True`` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. + :param root_path: The path to the root of the application files. + This should only be set manually when it can't be detected + automatically, such as for namespace packages. + """ + + default_config = ImmutableDict( + { + "DEBUG": None, + "TESTING": False, + "PROPAGATE_EXCEPTIONS": None, + "SECRET_KEY": None, + "PERMANENT_SESSION_LIFETIME": timedelta(days=31), + "USE_X_SENDFILE": False, + "SERVER_NAME": None, + "APPLICATION_ROOT": "/", + "SESSION_COOKIE_NAME": "session", + "SESSION_COOKIE_DOMAIN": None, + "SESSION_COOKIE_PATH": None, + "SESSION_COOKIE_HTTPONLY": True, + "SESSION_COOKIE_SECURE": False, + "SESSION_COOKIE_SAMESITE": None, + "SESSION_REFRESH_EACH_REQUEST": True, + "MAX_CONTENT_LENGTH": None, + "SEND_FILE_MAX_AGE_DEFAULT": None, + "TRAP_BAD_REQUEST_ERRORS": None, + "TRAP_HTTP_EXCEPTIONS": False, + "EXPLAIN_TEMPLATE_LOADING": False, + "PREFERRED_URL_SCHEME": "http", + "TEMPLATES_AUTO_RELOAD": None, + "MAX_COOKIE_SIZE": 4093, + } + ) + + #: The class that is used for request objects. See :class:`~flask.Request` + #: for more information. + request_class: type[Request] = Request + + #: The class that is used for response objects. See + #: :class:`~flask.Response` for more information. + response_class: type[Response] = Response + + #: the session interface to use. By default an instance of + #: :class:`~flask.sessions.SecureCookieSessionInterface` is used here. + #: + #: .. versionadded:: 0.8 + session_interface: SessionInterface = SecureCookieSessionInterface() + + def __init__( + self, + import_name: str, + static_url_path: str | None = None, + static_folder: str | os.PathLike[str] | None = "static", + static_host: str | None = None, + host_matching: bool = False, + subdomain_matching: bool = False, + template_folder: str | os.PathLike[str] | None = "templates", + instance_path: str | None = None, + instance_relative_config: bool = False, + root_path: str | None = None, + ): + super().__init__( + import_name=import_name, + static_url_path=static_url_path, + static_folder=static_folder, + static_host=static_host, + host_matching=host_matching, + subdomain_matching=subdomain_matching, + template_folder=template_folder, + instance_path=instance_path, + instance_relative_config=instance_relative_config, + root_path=root_path, + ) + + #: The Click command group for registering CLI commands for this + #: object. The commands are available from the ``flask`` command + #: once the application has been discovered and blueprints have + #: been registered. + self.cli = cli.AppGroup() + + # Set the name of the Click group in case someone wants to add + # the app's commands to another CLI tool. + self.cli.name = self.name + + # Add a static route using the provided static_url_path, static_host, + # and static_folder if there is a configured static_folder. + # Note we do this without checking if static_folder exists. + # For one, it might be created while the server is running (e.g. during + # development). Also, Google App Engine stores static files somewhere + if self.has_static_folder: + assert ( + bool(static_host) == host_matching + ), "Invalid static_host/host_matching combination" + # Use a weakref to avoid creating a reference cycle between the app + # and the view function (see #3761). + self_ref = weakref.ref(self) + self.add_url_rule( + f"{self.static_url_path}/", + endpoint="static", + host=static_host, + view_func=lambda **kw: self_ref().send_static_file(**kw), # type: ignore # noqa: B950 + ) + + def get_send_file_max_age(self, filename: str | None) -> int | None: + """Used by :func:`send_file` to determine the ``max_age`` cache + value for a given file path if it wasn't passed. + + By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from + the configuration of :data:`~flask.current_app`. This defaults + to ``None``, which tells the browser to use conditional requests + instead of a timed cache, which is usually preferable. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionchanged:: 2.0 + The default configuration is ``None`` instead of 12 hours. + + .. versionadded:: 0.9 + """ + value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] + + if value is None: + return None + + if isinstance(value, timedelta): + return int(value.total_seconds()) + + return value # type: ignore[no-any-return] + + def send_static_file(self, filename: str) -> Response: + """The view function used to serve files from + :attr:`static_folder`. A route is automatically registered for + this view at :attr:`static_url_path` if :attr:`static_folder` is + set. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionadded:: 0.5 + + """ + if not self.has_static_folder: + raise RuntimeError("'static_folder' must be set to serve static_files.") + + # send_file only knows to call get_send_file_max_age on the app, + # call it here so it works for blueprints too. + max_age = self.get_send_file_max_age(filename) + return send_from_directory( + t.cast(str, self.static_folder), filename, max_age=max_age + ) + + def open_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: + """Open a resource file relative to :attr:`root_path` for + reading. + + For example, if the file ``schema.sql`` is next to the file + ``app.py`` where the ``Flask`` app is defined, it can be opened + with: + + .. code-block:: python + + with app.open_resource("schema.sql") as f: + conn.executescript(f.read()) + + :param resource: Path to the resource relative to + :attr:`root_path`. + :param mode: Open the file in this mode. Only reading is + supported, valid values are "r" (or "rt") and "rb". + + Note this is a duplicate of the same method in the Flask + class. + + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + return open(os.path.join(self.root_path, resource), mode) + + def open_instance_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: + """Opens a resource from the application's instance folder + (:attr:`instance_path`). Otherwise works like + :meth:`open_resource`. Instance resources can also be opened for + writing. + + :param resource: the name of the resource. To access resources within + subfolders use forward slashes as separator. + :param mode: resource file opening mode, default is 'rb'. + """ + return open(os.path.join(self.instance_path, resource), mode) + + def create_jinja_environment(self) -> Environment: + """Create the Jinja environment based on :attr:`jinja_options` + and the various Jinja-related methods of the app. Changing + :attr:`jinja_options` after this will have no effect. Also adds + Flask-related globals and filters to the environment. + + .. versionchanged:: 0.11 + ``Environment.auto_reload`` set in accordance with + ``TEMPLATES_AUTO_RELOAD`` configuration option. + + .. versionadded:: 0.5 + """ + options = dict(self.jinja_options) + + if "autoescape" not in options: + options["autoescape"] = self.select_jinja_autoescape + + if "auto_reload" not in options: + auto_reload = self.config["TEMPLATES_AUTO_RELOAD"] + + if auto_reload is None: + auto_reload = self.debug + + options["auto_reload"] = auto_reload + + rv = self.jinja_environment(self, **options) + rv.globals.update( + url_for=self.url_for, + get_flashed_messages=get_flashed_messages, + config=self.config, + # request, session and g are normally added with the + # context processor for efficiency reasons but for imported + # templates we also want the proxies in there. + request=request, + session=session, + g=g, + ) + rv.policies["json.dumps_function"] = self.json.dumps + return rv + + def create_url_adapter(self, request: Request | None) -> MapAdapter | None: + """Creates a URL adapter for the given request. The URL adapter + is created at a point where the request context is not yet set + up so the request is passed explicitly. + + .. versionadded:: 0.6 + + .. versionchanged:: 0.9 + This can now also be called without a request object when the + URL adapter is created for the application context. + + .. versionchanged:: 1.0 + :data:`SERVER_NAME` no longer implicitly enables subdomain + matching. Use :attr:`subdomain_matching` instead. + """ + if request is not None: + # If subdomain matching is disabled (the default), use the + # default subdomain in all cases. This should be the default + # in Werkzeug but it currently does not have that feature. + if not self.subdomain_matching: + subdomain = self.url_map.default_subdomain or None + else: + subdomain = None + + return self.url_map.bind_to_environ( + request.environ, + server_name=self.config["SERVER_NAME"], + subdomain=subdomain, + ) + # We need at the very least the server name to be set for this + # to work. + if self.config["SERVER_NAME"] is not None: + return self.url_map.bind( + self.config["SERVER_NAME"], + script_name=self.config["APPLICATION_ROOT"], + url_scheme=self.config["PREFERRED_URL_SCHEME"], + ) + + return None + + def raise_routing_exception(self, request: Request) -> t.NoReturn: + """Intercept routing exceptions and possibly do something else. + + In debug mode, intercept a routing redirect and replace it with + an error if the body will be discarded. + + With modern Werkzeug this shouldn't occur, since it now uses a + 308 status which tells the browser to resend the method and + body. + + .. versionchanged:: 2.1 + Don't intercept 307 and 308 redirects. + + :meta private: + :internal: + """ + if ( + not self.debug + or not isinstance(request.routing_exception, RequestRedirect) + or request.routing_exception.code in {307, 308} + or request.method in {"GET", "HEAD", "OPTIONS"} + ): + raise request.routing_exception # type: ignore[misc] + + from .debughelpers import FormDataRoutingRedirect + + raise FormDataRoutingRedirect(request) + + def update_template_context(self, context: dict[str, t.Any]) -> None: + """Update the template context with some commonly used variables. + This injects request, session, config and g into the template + context as well as everything template context processors want + to inject. Note that the as of Flask 0.6, the original values + in the context will not be overridden if a context processor + decides to return a value with the same key. + + :param context: the context as a dictionary that is updated in place + to add extra variables. + """ + names: t.Iterable[str | None] = (None,) + + # A template may be rendered outside a request context. + if request: + names = chain(names, reversed(request.blueprints)) + + # The values passed to render_template take precedence. Keep a + # copy to re-apply after all context functions. + orig_ctx = context.copy() + + for name in names: + if name in self.template_context_processors: + for func in self.template_context_processors[name]: + context.update(self.ensure_sync(func)()) + + context.update(orig_ctx) + + def make_shell_context(self) -> dict[str, t.Any]: + """Returns the shell context for an interactive shell for this + application. This runs all the registered shell context + processors. + + .. versionadded:: 0.11 + """ + rv = {"app": self, "g": g} + for processor in self.shell_context_processors: + rv.update(processor()) + return rv + + def run( + self, + host: str | None = None, + port: int | None = None, + debug: bool | None = None, + load_dotenv: bool = True, + **options: t.Any, + ) -> None: + """Runs the application on a local development server. + + Do not use ``run()`` in a production setting. It is not intended to + meet security and performance requirements for a production server. + Instead, see :doc:`/deploying/index` for WSGI server recommendations. + + If the :attr:`debug` flag is set the server will automatically reload + for code changes and show a debugger in case an exception happened. + + If you want to run the application in debug mode, but disable the + code execution on the interactive debugger, you can pass + ``use_evalex=False`` as parameter. This will keep the debugger's + traceback screen active, but disable code execution. + + It is not recommended to use this function for development with + automatic reloading as this is badly supported. Instead you should + be using the :command:`flask` command line script's ``run`` support. + + .. admonition:: Keep in Mind + + Flask will suppress any server error with a generic error page + unless it is in debug mode. As such to enable just the + interactive debugger without the code reloading, you have to + invoke :meth:`run` with ``debug=True`` and ``use_reloader=False``. + Setting ``use_debugger`` to ``True`` without being in debug mode + won't catch any exceptions because there won't be any to + catch. + + :param host: the hostname to listen on. Set this to ``'0.0.0.0'`` to + have the server available externally as well. Defaults to + ``'127.0.0.1'`` or the host in the ``SERVER_NAME`` config variable + if present. + :param port: the port of the webserver. Defaults to ``5000`` or the + port defined in the ``SERVER_NAME`` config variable if present. + :param debug: if given, enable or disable debug mode. See + :attr:`debug`. + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` + files to set environment variables. Will also change the working + directory to the directory containing the first file found. + :param options: the options to be forwarded to the underlying Werkzeug + server. See :func:`werkzeug.serving.run_simple` for more + information. + + .. versionchanged:: 1.0 + If installed, python-dotenv will be used to load environment + variables from :file:`.env` and :file:`.flaskenv` files. + + The :envvar:`FLASK_DEBUG` environment variable will override :attr:`debug`. + + Threaded mode is enabled by default. + + .. versionchanged:: 0.10 + The default port is now picked from the ``SERVER_NAME`` + variable. + """ + # Ignore this call so that it doesn't start another server if + # the 'flask run' command is used. + if os.environ.get("FLASK_RUN_FROM_CLI") == "true": + if not is_running_from_reloader(): + click.secho( + " * Ignoring a call to 'app.run()' that would block" + " the current 'flask' CLI command.\n" + " Only call 'app.run()' in an 'if __name__ ==" + ' "__main__"\' guard.', + fg="red", + ) + + return + + if get_load_dotenv(load_dotenv): + cli.load_dotenv() + + # if set, env var overrides existing value + if "FLASK_DEBUG" in os.environ: + self.debug = get_debug_flag() + + # debug passed to method overrides all other sources + if debug is not None: + self.debug = bool(debug) + + server_name = self.config.get("SERVER_NAME") + sn_host = sn_port = None + + if server_name: + sn_host, _, sn_port = server_name.partition(":") + + if not host: + if sn_host: + host = sn_host + else: + host = "127.0.0.1" + + if port or port == 0: + port = int(port) + elif sn_port: + port = int(sn_port) + else: + port = 5000 + + options.setdefault("use_reloader", self.debug) + options.setdefault("use_debugger", self.debug) + options.setdefault("threaded", True) + + cli.show_server_banner(self.debug, self.name) + + from werkzeug.serving import run_simple + + try: + run_simple(t.cast(str, host), port, self, **options) + finally: + # reset the first request information if the development server + # reset normally. This makes it possible to restart the server + # without reloader and that stuff from an interactive shell. + self._got_first_request = False + + def test_client(self, use_cookies: bool = True, **kwargs: t.Any) -> FlaskClient: + """Creates a test client for this application. For information + about unit testing head over to :doc:`/testing`. + + Note that if you are testing for assertions or exceptions in your + application code, you must set ``app.testing = True`` in order for the + exceptions to propagate to the test client. Otherwise, the exception + will be handled by the application (not visible to the test client) and + the only indication of an AssertionError or other exception will be a + 500 status code response to the test client. See the :attr:`testing` + attribute. For example:: + + app.testing = True + client = app.test_client() + + The test client can be used in a ``with`` block to defer the closing down + of the context until the end of the ``with`` block. This is useful if + you want to access the context locals for testing:: + + with app.test_client() as c: + rv = c.get('/?vodka=42') + assert request.args['vodka'] == '42' + + Additionally, you may pass optional keyword arguments that will then + be passed to the application's :attr:`test_client_class` constructor. + For example:: + + from flask.testing import FlaskClient + + class CustomClient(FlaskClient): + def __init__(self, *args, **kwargs): + self._authentication = kwargs.pop("authentication") + super(CustomClient,self).__init__( *args, **kwargs) + + app.test_client_class = CustomClient + client = app.test_client(authentication='Basic ....') + + See :class:`~flask.testing.FlaskClient` for more information. + + .. versionchanged:: 0.4 + added support for ``with`` block usage for the client. + + .. versionadded:: 0.7 + The `use_cookies` parameter was added as well as the ability + to override the client to be used by setting the + :attr:`test_client_class` attribute. + + .. versionchanged:: 0.11 + Added `**kwargs` to support passing additional keyword arguments to + the constructor of :attr:`test_client_class`. + """ + cls = self.test_client_class + if cls is None: + from .testing import FlaskClient as cls + return cls( # type: ignore + self, self.response_class, use_cookies=use_cookies, **kwargs + ) + + def test_cli_runner(self, **kwargs: t.Any) -> FlaskCliRunner: + """Create a CLI runner for testing CLI commands. + See :ref:`testing-cli`. + + Returns an instance of :attr:`test_cli_runner_class`, by default + :class:`~flask.testing.FlaskCliRunner`. The Flask app object is + passed as the first argument. + + .. versionadded:: 1.0 + """ + cls = self.test_cli_runner_class + + if cls is None: + from .testing import FlaskCliRunner as cls + + return cls(self, **kwargs) # type: ignore + + def handle_http_exception( + self, e: HTTPException + ) -> HTTPException | ft.ResponseReturnValue: + """Handles an HTTP exception. By default this will invoke the + registered error handlers and fall back to returning the + exception as response. + + .. versionchanged:: 1.0.3 + ``RoutingException``, used internally for actions such as + slash redirects during routing, is not passed to error + handlers. + + .. versionchanged:: 1.0 + Exceptions are looked up by code *and* by MRO, so + ``HTTPException`` subclasses can be handled with a catch-all + handler for the base ``HTTPException``. + + .. versionadded:: 0.3 + """ + # Proxy exceptions don't have error codes. We want to always return + # those unchanged as errors + if e.code is None: + return e + + # RoutingExceptions are used internally to trigger routing + # actions, such as slash redirects raising RequestRedirect. They + # are not raised or handled in user code. + if isinstance(e, RoutingException): + return e + + handler = self._find_error_handler(e, request.blueprints) + if handler is None: + return e + return self.ensure_sync(handler)(e) # type: ignore[no-any-return] + + def handle_user_exception( + self, e: Exception + ) -> HTTPException | ft.ResponseReturnValue: + """This method is called whenever an exception occurs that + should be handled. A special case is :class:`~werkzeug + .exceptions.HTTPException` which is forwarded to the + :meth:`handle_http_exception` method. This function will either + return a response value or reraise the exception with the same + traceback. + + .. versionchanged:: 1.0 + Key errors raised from request data like ``form`` show the + bad key in debug mode rather than a generic bad request + message. + + .. versionadded:: 0.7 + """ + if isinstance(e, BadRequestKeyError) and ( + self.debug or self.config["TRAP_BAD_REQUEST_ERRORS"] + ): + e.show_exception = True + + if isinstance(e, HTTPException) and not self.trap_http_exception(e): + return self.handle_http_exception(e) + + handler = self._find_error_handler(e, request.blueprints) + + if handler is None: + raise + + return self.ensure_sync(handler)(e) # type: ignore[no-any-return] + + def handle_exception(self, e: Exception) -> Response: + """Handle an exception that did not have an error handler + associated with it, or that was raised from an error handler. + This always causes a 500 ``InternalServerError``. + + Always sends the :data:`got_request_exception` signal. + + If :data:`PROPAGATE_EXCEPTIONS` is ``True``, such as in debug + mode, the error will be re-raised so that the debugger can + display it. Otherwise, the original exception is logged, and + an :exc:`~werkzeug.exceptions.InternalServerError` is returned. + + If an error handler is registered for ``InternalServerError`` or + ``500``, it will be used. For consistency, the handler will + always receive the ``InternalServerError``. The original + unhandled exception is available as ``e.original_exception``. + + .. versionchanged:: 1.1.0 + Always passes the ``InternalServerError`` instance to the + handler, setting ``original_exception`` to the unhandled + error. + + .. versionchanged:: 1.1.0 + ``after_request`` functions and other finalization is done + even for the default 500 response when there is no handler. + + .. versionadded:: 0.3 + """ + exc_info = sys.exc_info() + got_request_exception.send(self, _async_wrapper=self.ensure_sync, exception=e) + propagate = self.config["PROPAGATE_EXCEPTIONS"] + + if propagate is None: + propagate = self.testing or self.debug + + if propagate: + # Re-raise if called with an active exception, otherwise + # raise the passed in exception. + if exc_info[1] is e: + raise + + raise e + + self.log_exception(exc_info) + server_error: InternalServerError | ft.ResponseReturnValue + server_error = InternalServerError(original_exception=e) + handler = self._find_error_handler(server_error, request.blueprints) + + if handler is not None: + server_error = self.ensure_sync(handler)(server_error) + + return self.finalize_request(server_error, from_error_handler=True) + + def log_exception( + self, + exc_info: (tuple[type, BaseException, TracebackType] | tuple[None, None, None]), + ) -> None: + """Logs an exception. This is called by :meth:`handle_exception` + if debugging is disabled and right before the handler is called. + The default implementation logs the exception as error on the + :attr:`logger`. + + .. versionadded:: 0.8 + """ + self.logger.error( + f"Exception on {request.path} [{request.method}]", exc_info=exc_info + ) + + def dispatch_request(self) -> ft.ResponseReturnValue: + """Does the request dispatching. Matches the URL and returns the + return value of the view or error handler. This does not have to + be a response object. In order to convert the return value to a + proper response object, call :func:`make_response`. + + .. versionchanged:: 0.7 + This no longer does the exception handling, this code was + moved to the new :meth:`full_dispatch_request`. + """ + req = request_ctx.request + if req.routing_exception is not None: + self.raise_routing_exception(req) + rule: Rule = req.url_rule # type: ignore[assignment] + # if we provide automatic options for this URL and the + # request came with the OPTIONS method, reply automatically + if ( + getattr(rule, "provide_automatic_options", False) + and req.method == "OPTIONS" + ): + return self.make_default_options_response() + # otherwise dispatch to the handler for that endpoint + view_args: dict[str, t.Any] = req.view_args # type: ignore[assignment] + return self.ensure_sync(self.view_functions[rule.endpoint])(**view_args) # type: ignore[no-any-return] + + def full_dispatch_request(self) -> Response: + """Dispatches the request and on top of that performs request + pre and postprocessing as well as HTTP exception catching and + error handling. + + .. versionadded:: 0.7 + """ + self._got_first_request = True + + try: + request_started.send(self, _async_wrapper=self.ensure_sync) + rv = self.preprocess_request() + if rv is None: + rv = self.dispatch_request() + except Exception as e: + rv = self.handle_user_exception(e) + return self.finalize_request(rv) + + def finalize_request( + self, + rv: ft.ResponseReturnValue | HTTPException, + from_error_handler: bool = False, + ) -> Response: + """Given the return value from a view function this finalizes + the request by converting it into a response and invoking the + postprocessing functions. This is invoked for both normal + request dispatching as well as error handlers. + + Because this means that it might be called as a result of a + failure a special safe mode is available which can be enabled + with the `from_error_handler` flag. If enabled, failures in + response processing will be logged and otherwise ignored. + + :internal: + """ + response = self.make_response(rv) + try: + response = self.process_response(response) + request_finished.send( + self, _async_wrapper=self.ensure_sync, response=response + ) + except Exception: + if not from_error_handler: + raise + self.logger.exception( + "Request finalizing failed with an error while handling an error" + ) + return response + + def make_default_options_response(self) -> Response: + """This method is called to create the default ``OPTIONS`` response. + This can be changed through subclassing to change the default + behavior of ``OPTIONS`` responses. + + .. versionadded:: 0.7 + """ + adapter = request_ctx.url_adapter + methods = adapter.allowed_methods() # type: ignore[union-attr] + rv = self.response_class() + rv.allow.update(methods) + return rv + + def ensure_sync(self, func: t.Callable[..., t.Any]) -> t.Callable[..., t.Any]: + """Ensure that the function is synchronous for WSGI workers. + Plain ``def`` functions are returned as-is. ``async def`` + functions are wrapped to run and wait for the response. + + Override this method to change how the app runs async views. + + .. versionadded:: 2.0 + """ + if iscoroutinefunction(func): + return self.async_to_sync(func) + + return func + + def async_to_sync( + self, func: t.Callable[..., t.Coroutine[t.Any, t.Any, t.Any]] + ) -> t.Callable[..., t.Any]: + """Return a sync function that will run the coroutine function. + + .. code-block:: python + + result = app.async_to_sync(func)(*args, **kwargs) + + Override this method to change how the app converts async code + to be synchronously callable. + + .. versionadded:: 2.0 + """ + try: + from asgiref.sync import async_to_sync as asgiref_async_to_sync + except ImportError: + raise RuntimeError( + "Install Flask with the 'async' extra in order to use async views." + ) from None + + return asgiref_async_to_sync(func) + + def url_for( + self, + /, + endpoint: str, + *, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, + **values: t.Any, + ) -> str: + """Generate a URL to the given endpoint with the given values. + + This is called by :func:`flask.url_for`, and can be called + directly as well. + + An *endpoint* is the name of a URL rule, usually added with + :meth:`@app.route() `, and usually the same name as the + view function. A route defined in a :class:`~flask.Blueprint` + will prepend the blueprint's name separated by a ``.`` to the + endpoint. + + In some cases, such as email messages, you want URLs to include + the scheme and domain, like ``https://example.com/hello``. When + not in an active request, URLs will be external by default, but + this requires setting :data:`SERVER_NAME` so Flask knows what + domain to use. :data:`APPLICATION_ROOT` and + :data:`PREFERRED_URL_SCHEME` should also be configured as + needed. This config is only used when not in an active request. + + Functions can be decorated with :meth:`url_defaults` to modify + keyword arguments before the URL is built. + + If building fails for some reason, such as an unknown endpoint + or incorrect values, the app's :meth:`handle_url_build_error` + method is called. If that returns a string, that is returned, + otherwise a :exc:`~werkzeug.routing.BuildError` is raised. + + :param endpoint: The endpoint name associated with the URL to + generate. If this starts with a ``.``, the current blueprint + name (if any) will be used. + :param _anchor: If given, append this as ``#anchor`` to the URL. + :param _method: If given, generate the URL associated with this + method for the endpoint. + :param _scheme: If given, the URL will have this scheme if it + is external. + :param _external: If given, prefer the URL to be internal + (False) or require it to be external (True). External URLs + include the scheme and domain. When not in an active + request, URLs are external by default. + :param values: Values to use for the variable parts of the URL + rule. Unknown keys are appended as query string arguments, + like ``?a=b&c=d``. + + .. versionadded:: 2.2 + Moved from ``flask.url_for``, which calls this method. + """ + req_ctx = _cv_request.get(None) + + if req_ctx is not None: + url_adapter = req_ctx.url_adapter + blueprint_name = req_ctx.request.blueprint + + # If the endpoint starts with "." and the request matches a + # blueprint, the endpoint is relative to the blueprint. + if endpoint[:1] == ".": + if blueprint_name is not None: + endpoint = f"{blueprint_name}{endpoint}" + else: + endpoint = endpoint[1:] + + # When in a request, generate a URL without scheme and + # domain by default, unless a scheme is given. + if _external is None: + _external = _scheme is not None + else: + app_ctx = _cv_app.get(None) + + # If called by helpers.url_for, an app context is active, + # use its url_adapter. Otherwise, app.url_for was called + # directly, build an adapter. + if app_ctx is not None: + url_adapter = app_ctx.url_adapter + else: + url_adapter = self.create_url_adapter(None) + + if url_adapter is None: + raise RuntimeError( + "Unable to build URLs outside an active request" + " without 'SERVER_NAME' configured. Also configure" + " 'APPLICATION_ROOT' and 'PREFERRED_URL_SCHEME' as" + " needed." + ) + + # When outside a request, generate a URL with scheme and + # domain by default. + if _external is None: + _external = True + + # It is an error to set _scheme when _external=False, in order + # to avoid accidental insecure URLs. + if _scheme is not None and not _external: + raise ValueError("When specifying '_scheme', '_external' must be True.") + + self.inject_url_defaults(endpoint, values) + + try: + rv = url_adapter.build( # type: ignore[union-attr] + endpoint, + values, + method=_method, + url_scheme=_scheme, + force_external=_external, + ) + except BuildError as error: + values.update( + _anchor=_anchor, _method=_method, _scheme=_scheme, _external=_external + ) + return self.handle_url_build_error(error, endpoint, values) + + if _anchor is not None: + _anchor = _url_quote(_anchor, safe="%!#$&'()*+,/:;=?@") + rv = f"{rv}#{_anchor}" + + return rv + + def make_response(self, rv: ft.ResponseReturnValue) -> Response: + """Convert the return value from a view function to an instance of + :attr:`response_class`. + + :param rv: the return value from the view function. The view function + must return a response. Returning ``None``, or the view ending + without returning, is not allowed. The following types are allowed + for ``view_rv``: + + ``str`` + A response object is created with the string encoded to UTF-8 + as the body. + + ``bytes`` + A response object is created with the bytes as the body. + + ``dict`` + A dictionary that will be jsonify'd before being returned. + + ``list`` + A list that will be jsonify'd before being returned. + + ``generator`` or ``iterator`` + A generator that returns ``str`` or ``bytes`` to be + streamed as the response. + + ``tuple`` + Either ``(body, status, headers)``, ``(body, status)``, or + ``(body, headers)``, where ``body`` is any of the other types + allowed here, ``status`` is a string or an integer, and + ``headers`` is a dictionary or a list of ``(key, value)`` + tuples. If ``body`` is a :attr:`response_class` instance, + ``status`` overwrites the exiting value and ``headers`` are + extended. + + :attr:`response_class` + The object is returned unchanged. + + other :class:`~werkzeug.wrappers.Response` class + The object is coerced to :attr:`response_class`. + + :func:`callable` + The function is called as a WSGI application. The result is + used to create a response object. + + .. versionchanged:: 2.2 + A generator will be converted to a streaming response. + A list will be converted to a JSON response. + + .. versionchanged:: 1.1 + A dict will be converted to a JSON response. + + .. versionchanged:: 0.9 + Previously a tuple was interpreted as the arguments for the + response object. + """ + + status = headers = None + + # unpack tuple returns + if isinstance(rv, tuple): + len_rv = len(rv) + + # a 3-tuple is unpacked directly + if len_rv == 3: + rv, status, headers = rv # type: ignore[misc] + # decide if a 2-tuple has status or headers + elif len_rv == 2: + if isinstance(rv[1], (Headers, dict, tuple, list)): + rv, headers = rv + else: + rv, status = rv # type: ignore[assignment,misc] + # other sized tuples are not allowed + else: + raise TypeError( + "The view function did not return a valid response tuple." + " The tuple must have the form (body, status, headers)," + " (body, status), or (body, headers)." + ) + + # the body must not be None + if rv is None: + raise TypeError( + f"The view function for {request.endpoint!r} did not" + " return a valid response. The function either returned" + " None or ended without a return statement." + ) + + # make sure the body is an instance of the response class + if not isinstance(rv, self.response_class): + if isinstance(rv, (str, bytes, bytearray)) or isinstance(rv, cabc.Iterator): + # let the response class set the status and headers instead of + # waiting to do it manually, so that the class can handle any + # special logic + rv = self.response_class( + rv, + status=status, + headers=headers, # type: ignore[arg-type] + ) + status = headers = None + elif isinstance(rv, (dict, list)): + rv = self.json.response(rv) + elif isinstance(rv, BaseResponse) or callable(rv): + # evaluate a WSGI callable, or coerce a different response + # class to the correct type + try: + rv = self.response_class.force_type( + rv, # type: ignore[arg-type] + request.environ, + ) + except TypeError as e: + raise TypeError( + f"{e}\nThe view function did not return a valid" + " response. The return type must be a string," + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it" + f" was a {type(rv).__name__}." + ).with_traceback(sys.exc_info()[2]) from None + else: + raise TypeError( + "The view function did not return a valid" + " response. The return type must be a string," + " dict, list, tuple with headers or status," + " Response instance, or WSGI callable, but it was a" + f" {type(rv).__name__}." + ) + + rv = t.cast(Response, rv) + # prefer the status if it was provided + if status is not None: + if isinstance(status, (str, bytes, bytearray)): + rv.status = status + else: + rv.status_code = status + + # extend existing headers with provided headers + if headers: + rv.headers.update(headers) # type: ignore[arg-type] + + return rv + + def preprocess_request(self) -> ft.ResponseReturnValue | None: + """Called before the request is dispatched. Calls + :attr:`url_value_preprocessors` registered with the app and the + current blueprint (if any). Then calls :attr:`before_request_funcs` + registered with the app and the blueprint. + + If any :meth:`before_request` handler returns a non-None value, the + value is handled as if it was the return value from the view, and + further request handling is stopped. + """ + names = (None, *reversed(request.blueprints)) + + for name in names: + if name in self.url_value_preprocessors: + for url_func in self.url_value_preprocessors[name]: + url_func(request.endpoint, request.view_args) + + for name in names: + if name in self.before_request_funcs: + for before_func in self.before_request_funcs[name]: + rv = self.ensure_sync(before_func)() + + if rv is not None: + return rv # type: ignore[no-any-return] + + return None + + def process_response(self, response: Response) -> Response: + """Can be overridden in order to modify the response object + before it's sent to the WSGI server. By default this will + call all the :meth:`after_request` decorated functions. + + .. versionchanged:: 0.5 + As of Flask 0.5 the functions registered for after request + execution are called in reverse order of registration. + + :param response: a :attr:`response_class` object. + :return: a new response object or the same, has to be an + instance of :attr:`response_class`. + """ + ctx = request_ctx._get_current_object() # type: ignore[attr-defined] + + for func in ctx._after_request_functions: + response = self.ensure_sync(func)(response) + + for name in chain(request.blueprints, (None,)): + if name in self.after_request_funcs: + for func in reversed(self.after_request_funcs[name]): + response = self.ensure_sync(func)(response) + + if not self.session_interface.is_null_session(ctx.session): + self.session_interface.save_session(self, ctx.session, response) + + return response + + def do_teardown_request( + self, + exc: BaseException | None = _sentinel, # type: ignore[assignment] + ) -> None: + """Called after the request is dispatched and the response is + returned, right before the request context is popped. + + This calls all functions decorated with + :meth:`teardown_request`, and :meth:`Blueprint.teardown_request` + if a blueprint handled the request. Finally, the + :data:`request_tearing_down` signal is sent. + + This is called by + :meth:`RequestContext.pop() `, + which may be delayed during testing to maintain access to + resources. + + :param exc: An unhandled exception raised while dispatching the + request. Detected from the current exception information if + not passed. Passed to each teardown function. + + .. versionchanged:: 0.9 + Added the ``exc`` argument. + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for name in chain(request.blueprints, (None,)): + if name in self.teardown_request_funcs: + for func in reversed(self.teardown_request_funcs[name]): + self.ensure_sync(func)(exc) + + request_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + def do_teardown_appcontext( + self, + exc: BaseException | None = _sentinel, # type: ignore[assignment] + ) -> None: + """Called right before the application context is popped. + + When handling a request, the application context is popped + after the request context. See :meth:`do_teardown_request`. + + This calls all functions decorated with + :meth:`teardown_appcontext`. Then the + :data:`appcontext_tearing_down` signal is sent. + + This is called by + :meth:`AppContext.pop() `. + + .. versionadded:: 0.9 + """ + if exc is _sentinel: + exc = sys.exc_info()[1] + + for func in reversed(self.teardown_appcontext_funcs): + self.ensure_sync(func)(exc) + + appcontext_tearing_down.send(self, _async_wrapper=self.ensure_sync, exc=exc) + + def app_context(self) -> AppContext: + """Create an :class:`~flask.ctx.AppContext`. Use as a ``with`` + block to push the context, which will make :data:`current_app` + point at this application. + + An application context is automatically pushed by + :meth:`RequestContext.push() ` + when handling a request, and when running a CLI command. Use + this to manually create a context outside of these situations. + + :: + + with app.app_context(): + init_db() + + See :doc:`/appcontext`. + + .. versionadded:: 0.9 + """ + return AppContext(self) + + def request_context(self, environ: WSGIEnvironment) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` representing a + WSGI environment. Use a ``with`` block to push the context, + which will make :data:`request` point at this request. + + See :doc:`/reqcontext`. + + Typically you should not call this from your own code. A request + context is automatically pushed by the :meth:`wsgi_app` when + handling a request. Use :meth:`test_request_context` to create + an environment and context instead of this method. + + :param environ: a WSGI environment + """ + return RequestContext(self, environ) + + def test_request_context(self, *args: t.Any, **kwargs: t.Any) -> RequestContext: + """Create a :class:`~flask.ctx.RequestContext` for a WSGI + environment created from the given values. This is mostly useful + during testing, where you may want to run a function that uses + request data without dispatching a full request. + + See :doc:`/reqcontext`. + + Use a ``with`` block to push the context, which will make + :data:`request` point at the request for the created + environment. :: + + with app.test_request_context(...): + generate_report() + + When using the shell, it may be easier to push and pop the + context manually to avoid indentation. :: + + ctx = app.test_request_context(...) + ctx.push() + ... + ctx.pop() + + Takes the same arguments as Werkzeug's + :class:`~werkzeug.test.EnvironBuilder`, with some defaults from + the application. See the linked Werkzeug docs for most of the + available arguments. Flask-specific behavior is listed here. + + :param path: URL path being requested. + :param base_url: Base URL where the app is being served, which + ``path`` is relative to. If not given, built from + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, + :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to append to + :data:`SERVER_NAME`. + :param url_scheme: Scheme to use instead of + :data:`PREFERRED_URL_SCHEME`. + :param data: The request body, either as a string or a dict of + form keys and values. + :param json: If given, this is serialized as JSON and passed as + ``data``. Also defaults ``content_type`` to + ``application/json``. + :param args: other positional arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + :param kwargs: other keyword arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + """ + from .testing import EnvironBuilder + + builder = EnvironBuilder(self, *args, **kwargs) + + try: + return self.request_context(builder.get_environ()) + finally: + builder.close() + + def wsgi_app( + self, environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: + """The actual WSGI application. This is not implemented in + :meth:`__call__` so that middlewares can be applied without + losing a reference to the app object. Instead of doing this:: + + app = MyMiddleware(app) + + It's a better idea to do this instead:: + + app.wsgi_app = MyMiddleware(app.wsgi_app) + + Then you still have the original application object around and + can continue to call methods on it. + + .. versionchanged:: 0.7 + Teardown events for the request and app contexts are called + even if an unhandled error occurs. Other events may not be + called depending on when an error occurs during dispatch. + See :ref:`callbacks-and-errors`. + + :param environ: A WSGI environment. + :param start_response: A callable accepting a status code, + a list of headers, and an optional exception context to + start the response. + """ + ctx = self.request_context(environ) + error: BaseException | None = None + try: + try: + ctx.push() + response = self.full_dispatch_request() + except Exception as e: + error = e + response = self.handle_exception(e) + except: # noqa: B001 + error = sys.exc_info()[1] + raise + return response(environ, start_response) + finally: + if "werkzeug.debug.preserve_context" in environ: + environ["werkzeug.debug.preserve_context"](_cv_app.get()) + environ["werkzeug.debug.preserve_context"](_cv_request.get()) + + if error is not None and self.should_ignore_error(error): + error = None + + ctx.pop(error) + + def __call__( + self, environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: + """The WSGI server calls the Flask application object as the + WSGI application. This calls :meth:`wsgi_app`, which can be + wrapped to apply middleware. + """ + return self.wsgi_app(environ, start_response) diff --git a/netdeploy/lib/python3.11/site-packages/flask/blueprints.py b/netdeploy/lib/python3.11/site-packages/flask/blueprints.py new file mode 100644 index 0000000..aa9eacf --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/blueprints.py @@ -0,0 +1,129 @@ +from __future__ import annotations + +import os +import typing as t +from datetime import timedelta + +from .cli import AppGroup +from .globals import current_app +from .helpers import send_from_directory +from .sansio.blueprints import Blueprint as SansioBlueprint +from .sansio.blueprints import BlueprintSetupState as BlueprintSetupState # noqa +from .sansio.scaffold import _sentinel + +if t.TYPE_CHECKING: # pragma: no cover + from .wrappers import Response + + +class Blueprint(SansioBlueprint): + def __init__( + self, + name: str, + import_name: str, + static_folder: str | os.PathLike[str] | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike[str] | None = None, + url_prefix: str | None = None, + subdomain: str | None = None, + url_defaults: dict[str, t.Any] | None = None, + root_path: str | None = None, + cli_group: str | None = _sentinel, # type: ignore + ) -> None: + super().__init__( + name, + import_name, + static_folder, + static_url_path, + template_folder, + url_prefix, + subdomain, + url_defaults, + root_path, + cli_group, + ) + + #: The Click command group for registering CLI commands for this + #: object. The commands are available from the ``flask`` command + #: once the application has been discovered and blueprints have + #: been registered. + self.cli = AppGroup() + + # Set the name of the Click group in case someone wants to add + # the app's commands to another CLI tool. + self.cli.name = self.name + + def get_send_file_max_age(self, filename: str | None) -> int | None: + """Used by :func:`send_file` to determine the ``max_age`` cache + value for a given file path if it wasn't passed. + + By default, this returns :data:`SEND_FILE_MAX_AGE_DEFAULT` from + the configuration of :data:`~flask.current_app`. This defaults + to ``None``, which tells the browser to use conditional requests + instead of a timed cache, which is usually preferable. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionchanged:: 2.0 + The default configuration is ``None`` instead of 12 hours. + + .. versionadded:: 0.9 + """ + value = current_app.config["SEND_FILE_MAX_AGE_DEFAULT"] + + if value is None: + return None + + if isinstance(value, timedelta): + return int(value.total_seconds()) + + return value # type: ignore[no-any-return] + + def send_static_file(self, filename: str) -> Response: + """The view function used to serve files from + :attr:`static_folder`. A route is automatically registered for + this view at :attr:`static_url_path` if :attr:`static_folder` is + set. + + Note this is a duplicate of the same method in the Flask + class. + + .. versionadded:: 0.5 + + """ + if not self.has_static_folder: + raise RuntimeError("'static_folder' must be set to serve static_files.") + + # send_file only knows to call get_send_file_max_age on the app, + # call it here so it works for blueprints too. + max_age = self.get_send_file_max_age(filename) + return send_from_directory( + t.cast(str, self.static_folder), filename, max_age=max_age + ) + + def open_resource(self, resource: str, mode: str = "rb") -> t.IO[t.AnyStr]: + """Open a resource file relative to :attr:`root_path` for + reading. + + For example, if the file ``schema.sql`` is next to the file + ``app.py`` where the ``Flask`` app is defined, it can be opened + with: + + .. code-block:: python + + with app.open_resource("schema.sql") as f: + conn.executescript(f.read()) + + :param resource: Path to the resource relative to + :attr:`root_path`. + :param mode: Open the file in this mode. Only reading is + supported, valid values are "r" (or "rt") and "rb". + + Note this is a duplicate of the same method in the Flask + class. + + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + return open(os.path.join(self.root_path, resource), mode) diff --git a/netdeploy/lib/python3.11/site-packages/flask/cli.py b/netdeploy/lib/python3.11/site-packages/flask/cli.py new file mode 100644 index 0000000..ecb292a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/cli.py @@ -0,0 +1,1109 @@ +from __future__ import annotations + +import ast +import collections.abc as cabc +import importlib.metadata +import inspect +import os +import platform +import re +import sys +import traceback +import typing as t +from functools import update_wrapper +from operator import itemgetter +from types import ModuleType + +import click +from click.core import ParameterSource +from werkzeug import run_simple +from werkzeug.serving import is_running_from_reloader +from werkzeug.utils import import_string + +from .globals import current_app +from .helpers import get_debug_flag +from .helpers import get_load_dotenv + +if t.TYPE_CHECKING: + import ssl + + from _typeshed.wsgi import StartResponse + from _typeshed.wsgi import WSGIApplication + from _typeshed.wsgi import WSGIEnvironment + + from .app import Flask + + +class NoAppException(click.UsageError): + """Raised if an application cannot be found or loaded.""" + + +def find_best_app(module: ModuleType) -> Flask: + """Given a module instance this tries to find the best possible + application in the module or raises an exception. + """ + from . import Flask + + # Search for the most common names first. + for attr_name in ("app", "application"): + app = getattr(module, attr_name, None) + + if isinstance(app, Flask): + return app + + # Otherwise find the only object that is a Flask instance. + matches = [v for v in module.__dict__.values() if isinstance(v, Flask)] + + if len(matches) == 1: + return matches[0] + elif len(matches) > 1: + raise NoAppException( + "Detected multiple Flask applications in module" + f" '{module.__name__}'. Use '{module.__name__}:name'" + " to specify the correct one." + ) + + # Search for app factory functions. + for attr_name in ("create_app", "make_app"): + app_factory = getattr(module, attr_name, None) + + if inspect.isfunction(app_factory): + try: + app = app_factory() + + if isinstance(app, Flask): + return app + except TypeError as e: + if not _called_with_wrong_args(app_factory): + raise + + raise NoAppException( + f"Detected factory '{attr_name}' in module '{module.__name__}'," + " but could not call it without arguments. Use" + f" '{module.__name__}:{attr_name}(args)'" + " to specify arguments." + ) from e + + raise NoAppException( + "Failed to find Flask application or factory in module" + f" '{module.__name__}'. Use '{module.__name__}:name'" + " to specify one." + ) + + +def _called_with_wrong_args(f: t.Callable[..., Flask]) -> bool: + """Check whether calling a function raised a ``TypeError`` because + the call failed or because something in the factory raised the + error. + + :param f: The function that was called. + :return: ``True`` if the call failed. + """ + tb = sys.exc_info()[2] + + try: + while tb is not None: + if tb.tb_frame.f_code is f.__code__: + # In the function, it was called successfully. + return False + + tb = tb.tb_next + + # Didn't reach the function. + return True + finally: + # Delete tb to break a circular reference. + # https://docs.python.org/2/library/sys.html#sys.exc_info + del tb + + +def find_app_by_string(module: ModuleType, app_name: str) -> Flask: + """Check if the given string is a variable name or a function. Call + a function to get the app instance, or return the variable directly. + """ + from . import Flask + + # Parse app_name as a single expression to determine if it's a valid + # attribute name or function call. + try: + expr = ast.parse(app_name.strip(), mode="eval").body + except SyntaxError: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) from None + + if isinstance(expr, ast.Name): + name = expr.id + args = [] + kwargs = {} + elif isinstance(expr, ast.Call): + # Ensure the function name is an attribute name only. + if not isinstance(expr.func, ast.Name): + raise NoAppException( + f"Function reference must be a simple name: {app_name!r}." + ) + + name = expr.func.id + + # Parse the positional and keyword arguments as literals. + try: + args = [ast.literal_eval(arg) for arg in expr.args] + kwargs = { + kw.arg: ast.literal_eval(kw.value) + for kw in expr.keywords + if kw.arg is not None + } + except ValueError: + # literal_eval gives cryptic error messages, show a generic + # message with the full expression instead. + raise NoAppException( + f"Failed to parse arguments as literal values: {app_name!r}." + ) from None + else: + raise NoAppException( + f"Failed to parse {app_name!r} as an attribute name or function call." + ) + + try: + attr = getattr(module, name) + except AttributeError as e: + raise NoAppException( + f"Failed to find attribute {name!r} in {module.__name__!r}." + ) from e + + # If the attribute is a function, call it with any args and kwargs + # to get the real application. + if inspect.isfunction(attr): + try: + app = attr(*args, **kwargs) + except TypeError as e: + if not _called_with_wrong_args(attr): + raise + + raise NoAppException( + f"The factory {app_name!r} in module" + f" {module.__name__!r} could not be called with the" + " specified arguments." + ) from e + else: + app = attr + + if isinstance(app, Flask): + return app + + raise NoAppException( + "A valid Flask application was not obtained from" + f" '{module.__name__}:{app_name}'." + ) + + +def prepare_import(path: str) -> str: + """Given a filename this will try to calculate the python path, add it + to the search path and return the actual module name that is expected. + """ + path = os.path.realpath(path) + + fname, ext = os.path.splitext(path) + if ext == ".py": + path = fname + + if os.path.basename(path) == "__init__": + path = os.path.dirname(path) + + module_name = [] + + # move up until outside package structure (no __init__.py) + while True: + path, name = os.path.split(path) + module_name.append(name) + + if not os.path.exists(os.path.join(path, "__init__.py")): + break + + if sys.path[0] != path: + sys.path.insert(0, path) + + return ".".join(module_name[::-1]) + + +@t.overload +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: t.Literal[True] = True +) -> Flask: ... + + +@t.overload +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: t.Literal[False] = ... +) -> Flask | None: ... + + +def locate_app( + module_name: str, app_name: str | None, raise_if_not_found: bool = True +) -> Flask | None: + try: + __import__(module_name) + except ImportError: + # Reraise the ImportError if it occurred within the imported module. + # Determine this by checking whether the trace has a depth > 1. + if sys.exc_info()[2].tb_next: # type: ignore[union-attr] + raise NoAppException( + f"While importing {module_name!r}, an ImportError was" + f" raised:\n\n{traceback.format_exc()}" + ) from None + elif raise_if_not_found: + raise NoAppException(f"Could not import {module_name!r}.") from None + else: + return None + + module = sys.modules[module_name] + + if app_name is None: + return find_best_app(module) + else: + return find_app_by_string(module, app_name) + + +def get_version(ctx: click.Context, param: click.Parameter, value: t.Any) -> None: + if not value or ctx.resilient_parsing: + return + + flask_version = importlib.metadata.version("flask") + werkzeug_version = importlib.metadata.version("werkzeug") + + click.echo( + f"Python {platform.python_version()}\n" + f"Flask {flask_version}\n" + f"Werkzeug {werkzeug_version}", + color=ctx.color, + ) + ctx.exit() + + +version_option = click.Option( + ["--version"], + help="Show the Flask version.", + expose_value=False, + callback=get_version, + is_flag=True, + is_eager=True, +) + + +class ScriptInfo: + """Helper object to deal with Flask applications. This is usually not + necessary to interface with as it's used internally in the dispatching + to click. In future versions of Flask this object will most likely play + a bigger role. Typically it's created automatically by the + :class:`FlaskGroup` but you can also manually create it and pass it + onwards as click object. + """ + + def __init__( + self, + app_import_path: str | None = None, + create_app: t.Callable[..., Flask] | None = None, + set_debug_flag: bool = True, + ) -> None: + #: Optionally the import path for the Flask application. + self.app_import_path = app_import_path + #: Optionally a function that is passed the script info to create + #: the instance of the application. + self.create_app = create_app + #: A dictionary with arbitrary data that can be associated with + #: this script info. + self.data: dict[t.Any, t.Any] = {} + self.set_debug_flag = set_debug_flag + self._loaded_app: Flask | None = None + + def load_app(self) -> Flask: + """Loads the Flask app (if not yet loaded) and returns it. Calling + this multiple times will just result in the already loaded app to + be returned. + """ + if self._loaded_app is not None: + return self._loaded_app + + if self.create_app is not None: + app: Flask | None = self.create_app() + else: + if self.app_import_path: + path, name = ( + re.split(r":(?![\\/])", self.app_import_path, maxsplit=1) + [None] + )[:2] + import_name = prepare_import(path) + app = locate_app(import_name, name) + else: + for path in ("wsgi.py", "app.py"): + import_name = prepare_import(path) + app = locate_app(import_name, None, raise_if_not_found=False) + + if app is not None: + break + + if app is None: + raise NoAppException( + "Could not locate a Flask application. Use the" + " 'flask --app' option, 'FLASK_APP' environment" + " variable, or a 'wsgi.py' or 'app.py' file in the" + " current directory." + ) + + if self.set_debug_flag: + # Update the app's debug flag through the descriptor so that + # other values repopulate as well. + app.debug = get_debug_flag() + + self._loaded_app = app + return app + + +pass_script_info = click.make_pass_decorator(ScriptInfo, ensure=True) + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + + +def with_appcontext(f: F) -> F: + """Wraps a callback so that it's guaranteed to be executed with the + script's application context. + + Custom commands (and their options) registered under ``app.cli`` or + ``blueprint.cli`` will always have an app context available, this + decorator is not required in that case. + + .. versionchanged:: 2.2 + The app context is active for subcommands as well as the + decorated callback. The app context is always available to + ``app.cli`` command and parameter callbacks. + """ + + @click.pass_context + def decorator(ctx: click.Context, /, *args: t.Any, **kwargs: t.Any) -> t.Any: + if not current_app: + app = ctx.ensure_object(ScriptInfo).load_app() + ctx.with_resource(app.app_context()) + + return ctx.invoke(f, *args, **kwargs) + + return update_wrapper(decorator, f) # type: ignore[return-value] + + +class AppGroup(click.Group): + """This works similar to a regular click :class:`~click.Group` but it + changes the behavior of the :meth:`command` decorator so that it + automatically wraps the functions in :func:`with_appcontext`. + + Not to be confused with :class:`FlaskGroup`. + """ + + def command( # type: ignore[override] + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], click.Command]: + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it wraps callbacks in :func:`with_appcontext` + unless it's disabled by passing ``with_appcontext=False``. + """ + wrap_for_ctx = kwargs.pop("with_appcontext", True) + + def decorator(f: t.Callable[..., t.Any]) -> click.Command: + if wrap_for_ctx: + f = with_appcontext(f) + return super(AppGroup, self).command(*args, **kwargs)(f) # type: ignore[no-any-return] + + return decorator + + def group( # type: ignore[override] + self, *args: t.Any, **kwargs: t.Any + ) -> t.Callable[[t.Callable[..., t.Any]], click.Group]: + """This works exactly like the method of the same name on a regular + :class:`click.Group` but it defaults the group class to + :class:`AppGroup`. + """ + kwargs.setdefault("cls", AppGroup) + return super().group(*args, **kwargs) # type: ignore[no-any-return] + + +def _set_app(ctx: click.Context, param: click.Option, value: str | None) -> str | None: + if value is None: + return None + + info = ctx.ensure_object(ScriptInfo) + info.app_import_path = value + return value + + +# This option is eager so the app will be available if --help is given. +# --help is also eager, so --app must be before it in the param list. +# no_args_is_help bypasses eager processing, so this option must be +# processed manually in that case to ensure FLASK_APP gets picked up. +_app_option = click.Option( + ["-A", "--app"], + metavar="IMPORT", + help=( + "The Flask application or factory function to load, in the form 'module:name'." + " Module can be a dotted import or file path. Name is not required if it is" + " 'app', 'application', 'create_app', or 'make_app', and can be 'name(args)' to" + " pass arguments." + ), + is_eager=True, + expose_value=False, + callback=_set_app, +) + + +def _set_debug(ctx: click.Context, param: click.Option, value: bool) -> bool | None: + # If the flag isn't provided, it will default to False. Don't use + # that, let debug be set by env in that case. + source = ctx.get_parameter_source(param.name) # type: ignore[arg-type] + + if source is not None and source in ( + ParameterSource.DEFAULT, + ParameterSource.DEFAULT_MAP, + ): + return None + + # Set with env var instead of ScriptInfo.load so that it can be + # accessed early during a factory function. + os.environ["FLASK_DEBUG"] = "1" if value else "0" + return value + + +_debug_option = click.Option( + ["--debug/--no-debug"], + help="Set debug mode.", + expose_value=False, + callback=_set_debug, +) + + +def _env_file_callback( + ctx: click.Context, param: click.Option, value: str | None +) -> str | None: + if value is None: + return None + + import importlib + + try: + importlib.import_module("dotenv") + except ImportError: + raise click.BadParameter( + "python-dotenv must be installed to load an env file.", + ctx=ctx, + param=param, + ) from None + + # Don't check FLASK_SKIP_DOTENV, that only disables automatically + # loading .env and .flaskenv files. + load_dotenv(value) + return value + + +# This option is eager so env vars are loaded as early as possible to be +# used by other options. +_env_file_option = click.Option( + ["-e", "--env-file"], + type=click.Path(exists=True, dir_okay=False), + help="Load environment variables from this file. python-dotenv must be installed.", + is_eager=True, + expose_value=False, + callback=_env_file_callback, +) + + +class FlaskGroup(AppGroup): + """Special subclass of the :class:`AppGroup` group that supports + loading more commands from the configured Flask app. Normally a + developer does not have to interface with this class but there are + some very advanced use cases for which it makes sense to create an + instance of this. see :ref:`custom-scripts`. + + :param add_default_commands: if this is True then the default run and + shell commands will be added. + :param add_version_option: adds the ``--version`` option. + :param create_app: an optional callback that is passed the script info and + returns the loaded app. + :param load_dotenv: Load the nearest :file:`.env` and :file:`.flaskenv` + files to set environment variables. Will also change the working + directory to the directory containing the first file found. + :param set_debug_flag: Set the app's debug flag. + + .. versionchanged:: 2.2 + Added the ``-A/--app``, ``--debug/--no-debug``, ``-e/--env-file`` options. + + .. versionchanged:: 2.2 + An app context is pushed when running ``app.cli`` commands, so + ``@with_appcontext`` is no longer required for those commands. + + .. versionchanged:: 1.0 + If installed, python-dotenv will be used to load environment variables + from :file:`.env` and :file:`.flaskenv` files. + """ + + def __init__( + self, + add_default_commands: bool = True, + create_app: t.Callable[..., Flask] | None = None, + add_version_option: bool = True, + load_dotenv: bool = True, + set_debug_flag: bool = True, + **extra: t.Any, + ) -> None: + params = list(extra.pop("params", None) or ()) + # Processing is done with option callbacks instead of a group + # callback. This allows users to make a custom group callback + # without losing the behavior. --env-file must come first so + # that it is eagerly evaluated before --app. + params.extend((_env_file_option, _app_option, _debug_option)) + + if add_version_option: + params.append(version_option) + + if "context_settings" not in extra: + extra["context_settings"] = {} + + extra["context_settings"].setdefault("auto_envvar_prefix", "FLASK") + + super().__init__(params=params, **extra) + + self.create_app = create_app + self.load_dotenv = load_dotenv + self.set_debug_flag = set_debug_flag + + if add_default_commands: + self.add_command(run_command) + self.add_command(shell_command) + self.add_command(routes_command) + + self._loaded_plugin_commands = False + + def _load_plugin_commands(self) -> None: + if self._loaded_plugin_commands: + return + + if sys.version_info >= (3, 10): + from importlib import metadata + else: + # Use a backport on Python < 3.10. We technically have + # importlib.metadata on 3.8+, but the API changed in 3.10, + # so use the backport for consistency. + import importlib_metadata as metadata + + for ep in metadata.entry_points(group="flask.commands"): + self.add_command(ep.load(), ep.name) + + self._loaded_plugin_commands = True + + def get_command(self, ctx: click.Context, name: str) -> click.Command | None: + self._load_plugin_commands() + # Look up built-in and plugin commands, which should be + # available even if the app fails to load. + rv = super().get_command(ctx, name) + + if rv is not None: + return rv + + info = ctx.ensure_object(ScriptInfo) + + # Look up commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. + try: + app = info.load_app() + except NoAppException as e: + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + return None + + # Push an app context for the loaded app unless it is already + # active somehow. This makes the context available to parameter + # and command callbacks without needing @with_appcontext. + if not current_app or current_app._get_current_object() is not app: # type: ignore[attr-defined] + ctx.with_resource(app.app_context()) + + return app.cli.get_command(ctx, name) + + def list_commands(self, ctx: click.Context) -> list[str]: + self._load_plugin_commands() + # Start with the built-in and plugin commands. + rv = set(super().list_commands(ctx)) + info = ctx.ensure_object(ScriptInfo) + + # Add commands provided by the app, showing an error and + # continuing if the app couldn't be loaded. + try: + rv.update(info.load_app().cli.list_commands(ctx)) + except NoAppException as e: + # When an app couldn't be loaded, show the error message + # without the traceback. + click.secho(f"Error: {e.format_message()}\n", err=True, fg="red") + except Exception: + # When any other errors occurred during loading, show the + # full traceback. + click.secho(f"{traceback.format_exc()}\n", err=True, fg="red") + + return sorted(rv) + + def make_context( + self, + info_name: str | None, + args: list[str], + parent: click.Context | None = None, + **extra: t.Any, + ) -> click.Context: + # Set a flag to tell app.run to become a no-op. If app.run was + # not in a __name__ == __main__ guard, it would start the server + # when importing, blocking whatever command is being called. + os.environ["FLASK_RUN_FROM_CLI"] = "true" + + # Attempt to load .env and .flask env files. The --env-file + # option can cause another file to be loaded. + if get_load_dotenv(self.load_dotenv): + load_dotenv() + + if "obj" not in extra and "obj" not in self.context_settings: + extra["obj"] = ScriptInfo( + create_app=self.create_app, set_debug_flag=self.set_debug_flag + ) + + return super().make_context(info_name, args, parent=parent, **extra) + + def parse_args(self, ctx: click.Context, args: list[str]) -> list[str]: + if not args and self.no_args_is_help: + # Attempt to load --env-file and --app early in case they + # were given as env vars. Otherwise no_args_is_help will not + # see commands from app.cli. + _env_file_option.handle_parse_result(ctx, {}, []) + _app_option.handle_parse_result(ctx, {}, []) + + return super().parse_args(ctx, args) + + +def _path_is_ancestor(path: str, other: str) -> bool: + """Take ``other`` and remove the length of ``path`` from it. Then join it + to ``path``. If it is the original value, ``path`` is an ancestor of + ``other``.""" + return os.path.join(path, other[len(path) :].lstrip(os.sep)) == other + + +def load_dotenv(path: str | os.PathLike[str] | None = None) -> bool: + """Load "dotenv" files in order of precedence to set environment variables. + + If an env var is already set it is not overwritten, so earlier files in the + list are preferred over later files. + + This is a no-op if `python-dotenv`_ is not installed. + + .. _python-dotenv: https://github.com/theskumar/python-dotenv#readme + + :param path: Load the file at this location instead of searching. + :return: ``True`` if a file was loaded. + + .. versionchanged:: 2.0 + The current directory is not changed to the location of the + loaded file. + + .. versionchanged:: 2.0 + When loading the env files, set the default encoding to UTF-8. + + .. versionchanged:: 1.1.0 + Returns ``False`` when python-dotenv is not installed, or when + the given path isn't a file. + + .. versionadded:: 1.0 + """ + try: + import dotenv + except ImportError: + if path or os.path.isfile(".env") or os.path.isfile(".flaskenv"): + click.secho( + " * Tip: There are .env or .flaskenv files present." + ' Do "pip install python-dotenv" to use them.', + fg="yellow", + err=True, + ) + + return False + + # Always return after attempting to load a given path, don't load + # the default files. + if path is not None: + if os.path.isfile(path): + return dotenv.load_dotenv(path, encoding="utf-8") + + return False + + loaded = False + + for name in (".env", ".flaskenv"): + path = dotenv.find_dotenv(name, usecwd=True) + + if not path: + continue + + dotenv.load_dotenv(path, encoding="utf-8") + loaded = True + + return loaded # True if at least one file was located and loaded. + + +def show_server_banner(debug: bool, app_import_path: str | None) -> None: + """Show extra startup messages the first time the server is run, + ignoring the reloader. + """ + if is_running_from_reloader(): + return + + if app_import_path is not None: + click.echo(f" * Serving Flask app '{app_import_path}'") + + if debug is not None: + click.echo(f" * Debug mode: {'on' if debug else 'off'}") + + +class CertParamType(click.ParamType): + """Click option type for the ``--cert`` option. Allows either an + existing file, the string ``'adhoc'``, or an import for a + :class:`~ssl.SSLContext` object. + """ + + name = "path" + + def __init__(self) -> None: + self.path_type = click.Path(exists=True, dir_okay=False, resolve_path=True) + + def convert( + self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None + ) -> t.Any: + try: + import ssl + except ImportError: + raise click.BadParameter( + 'Using "--cert" requires Python to be compiled with SSL support.', + ctx, + param, + ) from None + + try: + return self.path_type(value, param, ctx) + except click.BadParameter: + value = click.STRING(value, param, ctx).lower() + + if value == "adhoc": + try: + import cryptography # noqa: F401 + except ImportError: + raise click.BadParameter( + "Using ad-hoc certificates requires the cryptography library.", + ctx, + param, + ) from None + + return value + + obj = import_string(value, silent=True) + + if isinstance(obj, ssl.SSLContext): + return obj + + raise + + +def _validate_key(ctx: click.Context, param: click.Parameter, value: t.Any) -> t.Any: + """The ``--key`` option must be specified when ``--cert`` is a file. + Modifies the ``cert`` param to be a ``(cert, key)`` pair if needed. + """ + cert = ctx.params.get("cert") + is_adhoc = cert == "adhoc" + + try: + import ssl + except ImportError: + is_context = False + else: + is_context = isinstance(cert, ssl.SSLContext) + + if value is not None: + if is_adhoc: + raise click.BadParameter( + 'When "--cert" is "adhoc", "--key" is not used.', ctx, param + ) + + if is_context: + raise click.BadParameter( + 'When "--cert" is an SSLContext object, "--key" is not used.', + ctx, + param, + ) + + if not cert: + raise click.BadParameter('"--cert" must also be specified.', ctx, param) + + ctx.params["cert"] = cert, value + + else: + if cert and not (is_adhoc or is_context): + raise click.BadParameter('Required when using "--cert".', ctx, param) + + return value + + +class SeparatedPathType(click.Path): + """Click option type that accepts a list of values separated by the + OS's path separator (``:``, ``;`` on Windows). Each value is + validated as a :class:`click.Path` type. + """ + + def convert( + self, value: t.Any, param: click.Parameter | None, ctx: click.Context | None + ) -> t.Any: + items = self.split_envvar_value(value) + # can't call no-arg super() inside list comprehension until Python 3.12 + super_convert = super().convert + return [super_convert(item, param, ctx) for item in items] + + +@click.command("run", short_help="Run a development server.") +@click.option("--host", "-h", default="127.0.0.1", help="The interface to bind to.") +@click.option("--port", "-p", default=5000, help="The port to bind to.") +@click.option( + "--cert", + type=CertParamType(), + help="Specify a certificate file to use HTTPS.", + is_eager=True, +) +@click.option( + "--key", + type=click.Path(exists=True, dir_okay=False, resolve_path=True), + callback=_validate_key, + expose_value=False, + help="The key file to use when specifying a certificate.", +) +@click.option( + "--reload/--no-reload", + default=None, + help="Enable or disable the reloader. By default the reloader " + "is active if debug is enabled.", +) +@click.option( + "--debugger/--no-debugger", + default=None, + help="Enable or disable the debugger. By default the debugger " + "is active if debug is enabled.", +) +@click.option( + "--with-threads/--without-threads", + default=True, + help="Enable or disable multithreading.", +) +@click.option( + "--extra-files", + default=None, + type=SeparatedPathType(), + help=( + "Extra files that trigger a reload on change. Multiple paths" + f" are separated by {os.path.pathsep!r}." + ), +) +@click.option( + "--exclude-patterns", + default=None, + type=SeparatedPathType(), + help=( + "Files matching these fnmatch patterns will not trigger a reload" + " on change. Multiple patterns are separated by" + f" {os.path.pathsep!r}." + ), +) +@pass_script_info +def run_command( + info: ScriptInfo, + host: str, + port: int, + reload: bool, + debugger: bool, + with_threads: bool, + cert: ssl.SSLContext | tuple[str, str | None] | t.Literal["adhoc"] | None, + extra_files: list[str] | None, + exclude_patterns: list[str] | None, +) -> None: + """Run a local development server. + + This server is for development purposes only. It does not provide + the stability, security, or performance of production WSGI servers. + + The reloader and debugger are enabled by default with the '--debug' + option. + """ + try: + app: WSGIApplication = info.load_app() + except Exception as e: + if is_running_from_reloader(): + # When reloading, print out the error immediately, but raise + # it later so the debugger or server can handle it. + traceback.print_exc() + err = e + + def app( + environ: WSGIEnvironment, start_response: StartResponse + ) -> cabc.Iterable[bytes]: + raise err from None + + else: + # When not reloading, raise the error immediately so the + # command fails. + raise e from None + + debug = get_debug_flag() + + if reload is None: + reload = debug + + if debugger is None: + debugger = debug + + show_server_banner(debug, info.app_import_path) + + run_simple( + host, + port, + app, + use_reloader=reload, + use_debugger=debugger, + threaded=with_threads, + ssl_context=cert, + extra_files=extra_files, + exclude_patterns=exclude_patterns, + ) + + +run_command.params.insert(0, _debug_option) + + +@click.command("shell", short_help="Run a shell in the app context.") +@with_appcontext +def shell_command() -> None: + """Run an interactive Python shell in the context of a given + Flask application. The application will populate the default + namespace of this shell according to its configuration. + + This is useful for executing small snippets of management code + without having to manually configure the application. + """ + import code + + banner = ( + f"Python {sys.version} on {sys.platform}\n" + f"App: {current_app.import_name}\n" + f"Instance: {current_app.instance_path}" + ) + ctx: dict[str, t.Any] = {} + + # Support the regular Python interpreter startup script if someone + # is using it. + startup = os.environ.get("PYTHONSTARTUP") + if startup and os.path.isfile(startup): + with open(startup) as f: + eval(compile(f.read(), startup, "exec"), ctx) + + ctx.update(current_app.make_shell_context()) + + # Site, customize, or startup script can set a hook to call when + # entering interactive mode. The default one sets up readline with + # tab and history completion. + interactive_hook = getattr(sys, "__interactivehook__", None) + + if interactive_hook is not None: + try: + import readline + from rlcompleter import Completer + except ImportError: + pass + else: + # rlcompleter uses __main__.__dict__ by default, which is + # flask.__main__. Use the shell context instead. + readline.set_completer(Completer(ctx).complete) + + interactive_hook() + + code.interact(banner=banner, local=ctx) + + +@click.command("routes", short_help="Show the routes for the app.") +@click.option( + "--sort", + "-s", + type=click.Choice(("endpoint", "methods", "domain", "rule", "match")), + default="endpoint", + help=( + "Method to sort routes by. 'match' is the order that Flask will match routes" + " when dispatching a request." + ), +) +@click.option("--all-methods", is_flag=True, help="Show HEAD and OPTIONS methods.") +@with_appcontext +def routes_command(sort: str, all_methods: bool) -> None: + """Show all registered routes with endpoints and methods.""" + rules = list(current_app.url_map.iter_rules()) + + if not rules: + click.echo("No routes were registered.") + return + + ignored_methods = set() if all_methods else {"HEAD", "OPTIONS"} + host_matching = current_app.url_map.host_matching + has_domain = any(rule.host if host_matching else rule.subdomain for rule in rules) + rows = [] + + for rule in rules: + row = [ + rule.endpoint, + ", ".join(sorted((rule.methods or set()) - ignored_methods)), + ] + + if has_domain: + row.append((rule.host if host_matching else rule.subdomain) or "") + + row.append(rule.rule) + rows.append(row) + + headers = ["Endpoint", "Methods"] + sorts = ["endpoint", "methods"] + + if has_domain: + headers.append("Host" if host_matching else "Subdomain") + sorts.append("domain") + + headers.append("Rule") + sorts.append("rule") + + try: + rows.sort(key=itemgetter(sorts.index(sort))) + except ValueError: + pass + + rows.insert(0, headers) + widths = [max(len(row[i]) for row in rows) for i in range(len(headers))] + rows.insert(1, ["-" * w for w in widths]) + template = " ".join(f"{{{i}:<{w}}}" for i, w in enumerate(widths)) + + for row in rows: + click.echo(template.format(*row)) + + +cli = FlaskGroup( + name="flask", + help="""\ +A general utility script for Flask applications. + +An application to load must be given with the '--app' option, +'FLASK_APP' environment variable, or with a 'wsgi.py' or 'app.py' file +in the current directory. +""", +) + + +def main() -> None: + cli.main() + + +if __name__ == "__main__": + main() diff --git a/netdeploy/lib/python3.11/site-packages/flask/config.py b/netdeploy/lib/python3.11/site-packages/flask/config.py new file mode 100644 index 0000000..7e3ba17 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/config.py @@ -0,0 +1,370 @@ +from __future__ import annotations + +import errno +import json +import os +import types +import typing as t + +from werkzeug.utils import import_string + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .sansio.app import App + + +T = t.TypeVar("T") + + +class ConfigAttribute(t.Generic[T]): + """Makes an attribute forward to the config""" + + def __init__( + self, name: str, get_converter: t.Callable[[t.Any], T] | None = None + ) -> None: + self.__name__ = name + self.get_converter = get_converter + + @t.overload + def __get__(self, obj: None, owner: None) -> te.Self: ... + + @t.overload + def __get__(self, obj: App, owner: type[App]) -> T: ... + + def __get__(self, obj: App | None, owner: type[App] | None = None) -> T | te.Self: + if obj is None: + return self + + rv = obj.config[self.__name__] + + if self.get_converter is not None: + rv = self.get_converter(rv) + + return rv # type: ignore[no-any-return] + + def __set__(self, obj: App, value: t.Any) -> None: + obj.config[self.__name__] = value + + +class Config(dict): # type: ignore[type-arg] + """Works exactly like a dict but provides ways to fill it from files + or special dictionaries. There are two common patterns to populate the + config. + + Either you can fill the config from a config file:: + + app.config.from_pyfile('yourconfig.cfg') + + Or alternatively you can define the configuration options in the + module that calls :meth:`from_object` or provide an import path to + a module that should be loaded. It is also possible to tell it to + use the same module and with that provide the configuration values + just before the call:: + + DEBUG = True + SECRET_KEY = 'development key' + app.config.from_object(__name__) + + In both cases (loading from any Python file or loading from modules), + only uppercase keys are added to the config. This makes it possible to use + lowercase values in the config file for temporary values that are not added + to the config or to define the config keys in the same file that implements + the application. + + Probably the most interesting way to load configurations is from an + environment variable pointing to a file:: + + app.config.from_envvar('YOURAPPLICATION_SETTINGS') + + In this case before launching the application you have to set this + environment variable to the file you want to use. On Linux and OS X + use the export statement:: + + export YOURAPPLICATION_SETTINGS='/path/to/config/file' + + On windows use `set` instead. + + :param root_path: path to which files are read relative from. When the + config object is created by the application, this is + the application's :attr:`~flask.Flask.root_path`. + :param defaults: an optional dictionary of default values + """ + + def __init__( + self, + root_path: str | os.PathLike[str], + defaults: dict[str, t.Any] | None = None, + ) -> None: + super().__init__(defaults or {}) + self.root_path = root_path + + def from_envvar(self, variable_name: str, silent: bool = False) -> bool: + """Loads a configuration from an environment variable pointing to + a configuration file. This is basically just a shortcut with nicer + error messages for this line of code:: + + app.config.from_pyfile(os.environ['YOURAPPLICATION_SETTINGS']) + + :param variable_name: name of the environment variable + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: ``True`` if the file was loaded successfully. + """ + rv = os.environ.get(variable_name) + if not rv: + if silent: + return False + raise RuntimeError( + f"The environment variable {variable_name!r} is not set" + " and as such configuration could not be loaded. Set" + " this variable and make it point to a configuration" + " file" + ) + return self.from_pyfile(rv, silent=silent) + + def from_prefixed_env( + self, prefix: str = "FLASK", *, loads: t.Callable[[str], t.Any] = json.loads + ) -> bool: + """Load any environment variables that start with ``FLASK_``, + dropping the prefix from the env key for the config key. Values + are passed through a loading function to attempt to convert them + to more specific types than strings. + + Keys are loaded in :func:`sorted` order. + + The default loading function attempts to parse values as any + valid JSON type, including dicts and lists. + + Specific items in nested dicts can be set by separating the + keys with double underscores (``__``). If an intermediate key + doesn't exist, it will be initialized to an empty dict. + + :param prefix: Load env vars that start with this prefix, + separated with an underscore (``_``). + :param loads: Pass each string value to this function and use + the returned value as the config value. If any error is + raised it is ignored and the value remains a string. The + default is :func:`json.loads`. + + .. versionadded:: 2.1 + """ + prefix = f"{prefix}_" + len_prefix = len(prefix) + + for key in sorted(os.environ): + if not key.startswith(prefix): + continue + + value = os.environ[key] + + try: + value = loads(value) + except Exception: + # Keep the value as a string if loading failed. + pass + + # Change to key.removeprefix(prefix) on Python >= 3.9. + key = key[len_prefix:] + + if "__" not in key: + # A non-nested key, set directly. + self[key] = value + continue + + # Traverse nested dictionaries with keys separated by "__". + current = self + *parts, tail = key.split("__") + + for part in parts: + # If an intermediate dict does not exist, create it. + if part not in current: + current[part] = {} + + current = current[part] + + current[tail] = value + + return True + + def from_pyfile( + self, filename: str | os.PathLike[str], silent: bool = False + ) -> bool: + """Updates the values in the config from a Python file. This function + behaves as if the file was imported as module with the + :meth:`from_object` function. + + :param filename: the filename of the config. This can either be an + absolute filename or a filename relative to the + root path. + :param silent: set to ``True`` if you want silent failure for missing + files. + :return: ``True`` if the file was loaded successfully. + + .. versionadded:: 0.7 + `silent` parameter. + """ + filename = os.path.join(self.root_path, filename) + d = types.ModuleType("config") + d.__file__ = filename + try: + with open(filename, mode="rb") as config_file: + exec(compile(config_file.read(), filename, "exec"), d.__dict__) + except OSError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR, errno.ENOTDIR): + return False + e.strerror = f"Unable to load configuration file ({e.strerror})" + raise + self.from_object(d) + return True + + def from_object(self, obj: object | str) -> None: + """Updates the values from the given object. An object can be of one + of the following two types: + + - a string: in this case the object with that name will be imported + - an actual object reference: that object is used directly + + Objects are usually either modules or classes. :meth:`from_object` + loads only the uppercase attributes of the module/class. A ``dict`` + object will not work with :meth:`from_object` because the keys of a + ``dict`` are not attributes of the ``dict`` class. + + Example of module-based configuration:: + + app.config.from_object('yourapplication.default_config') + from yourapplication import default_config + app.config.from_object(default_config) + + Nothing is done to the object before loading. If the object is a + class and has ``@property`` attributes, it needs to be + instantiated before being passed to this method. + + You should not use this function to load the actual configuration but + rather configuration defaults. The actual config should be loaded + with :meth:`from_pyfile` and ideally from a location not within the + package because the package might be installed system wide. + + See :ref:`config-dev-prod` for an example of class-based configuration + using :meth:`from_object`. + + :param obj: an import name or object + """ + if isinstance(obj, str): + obj = import_string(obj) + for key in dir(obj): + if key.isupper(): + self[key] = getattr(obj, key) + + def from_file( + self, + filename: str | os.PathLike[str], + load: t.Callable[[t.IO[t.Any]], t.Mapping[str, t.Any]], + silent: bool = False, + text: bool = True, + ) -> bool: + """Update the values in the config from a file that is loaded + using the ``load`` parameter. The loaded data is passed to the + :meth:`from_mapping` method. + + .. code-block:: python + + import json + app.config.from_file("config.json", load=json.load) + + import tomllib + app.config.from_file("config.toml", load=tomllib.load, text=False) + + :param filename: The path to the data file. This can be an + absolute path or relative to the config root path. + :param load: A callable that takes a file handle and returns a + mapping of loaded data from the file. + :type load: ``Callable[[Reader], Mapping]`` where ``Reader`` + implements a ``read`` method. + :param silent: Ignore the file if it doesn't exist. + :param text: Open the file in text or binary mode. + :return: ``True`` if the file was loaded successfully. + + .. versionchanged:: 2.3 + The ``text`` parameter was added. + + .. versionadded:: 2.0 + """ + filename = os.path.join(self.root_path, filename) + + try: + with open(filename, "r" if text else "rb") as f: + obj = load(f) + except OSError as e: + if silent and e.errno in (errno.ENOENT, errno.EISDIR): + return False + + e.strerror = f"Unable to load configuration file ({e.strerror})" + raise + + return self.from_mapping(obj) + + def from_mapping( + self, mapping: t.Mapping[str, t.Any] | None = None, **kwargs: t.Any + ) -> bool: + """Updates the config like :meth:`update` ignoring items with + non-upper keys. + + :return: Always returns ``True``. + + .. versionadded:: 0.11 + """ + mappings: dict[str, t.Any] = {} + if mapping is not None: + mappings.update(mapping) + mappings.update(kwargs) + for key, value in mappings.items(): + if key.isupper(): + self[key] = value + return True + + def get_namespace( + self, namespace: str, lowercase: bool = True, trim_namespace: bool = True + ) -> dict[str, t.Any]: + """Returns a dictionary containing a subset of configuration options + that match the specified namespace/prefix. Example usage:: + + app.config['IMAGE_STORE_TYPE'] = 'fs' + app.config['IMAGE_STORE_PATH'] = '/var/app/images' + app.config['IMAGE_STORE_BASE_URL'] = 'http://img.website.com' + image_store_config = app.config.get_namespace('IMAGE_STORE_') + + The resulting dictionary `image_store_config` would look like:: + + { + 'type': 'fs', + 'path': '/var/app/images', + 'base_url': 'http://img.website.com' + } + + This is often useful when configuration options map directly to + keyword arguments in functions or class constructors. + + :param namespace: a configuration namespace + :param lowercase: a flag indicating if the keys of the resulting + dictionary should be lowercase + :param trim_namespace: a flag indicating if the keys of the resulting + dictionary should not include the namespace + + .. versionadded:: 0.11 + """ + rv = {} + for k, v in self.items(): + if not k.startswith(namespace): + continue + if trim_namespace: + key = k[len(namespace) :] + else: + key = k + if lowercase: + key = key.lower() + rv[key] = v + return rv + + def __repr__(self) -> str: + return f"<{type(self).__name__} {dict.__repr__(self)}>" diff --git a/netdeploy/lib/python3.11/site-packages/flask/ctx.py b/netdeploy/lib/python3.11/site-packages/flask/ctx.py new file mode 100644 index 0000000..9b164d3 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/ctx.py @@ -0,0 +1,449 @@ +from __future__ import annotations + +import contextvars +import sys +import typing as t +from functools import update_wrapper +from types import TracebackType + +from werkzeug.exceptions import HTTPException + +from . import typing as ft +from .globals import _cv_app +from .globals import _cv_request +from .signals import appcontext_popped +from .signals import appcontext_pushed + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import WSGIEnvironment + + from .app import Flask + from .sessions import SessionMixin + from .wrappers import Request + + +# a singleton sentinel value for parameter defaults +_sentinel = object() + + +class _AppCtxGlobals: + """A plain object. Used as a namespace for storing data during an + application context. + + Creating an app context automatically creates this object, which is + made available as the :data:`g` proxy. + + .. describe:: 'key' in g + + Check whether an attribute is present. + + .. versionadded:: 0.10 + + .. describe:: iter(g) + + Return an iterator over the attribute names. + + .. versionadded:: 0.10 + """ + + # Define attr methods to let mypy know this is a namespace object + # that has arbitrary attributes. + + def __getattr__(self, name: str) -> t.Any: + try: + return self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def __setattr__(self, name: str, value: t.Any) -> None: + self.__dict__[name] = value + + def __delattr__(self, name: str) -> None: + try: + del self.__dict__[name] + except KeyError: + raise AttributeError(name) from None + + def get(self, name: str, default: t.Any | None = None) -> t.Any: + """Get an attribute by name, or a default value. Like + :meth:`dict.get`. + + :param name: Name of attribute to get. + :param default: Value to return if the attribute is not present. + + .. versionadded:: 0.10 + """ + return self.__dict__.get(name, default) + + def pop(self, name: str, default: t.Any = _sentinel) -> t.Any: + """Get and remove an attribute by name. Like :meth:`dict.pop`. + + :param name: Name of attribute to pop. + :param default: Value to return if the attribute is not present, + instead of raising a ``KeyError``. + + .. versionadded:: 0.11 + """ + if default is _sentinel: + return self.__dict__.pop(name) + else: + return self.__dict__.pop(name, default) + + def setdefault(self, name: str, default: t.Any = None) -> t.Any: + """Get the value of an attribute if it is present, otherwise + set and return a default value. Like :meth:`dict.setdefault`. + + :param name: Name of attribute to get. + :param default: Value to set and return if the attribute is not + present. + + .. versionadded:: 0.11 + """ + return self.__dict__.setdefault(name, default) + + def __contains__(self, item: str) -> bool: + return item in self.__dict__ + + def __iter__(self) -> t.Iterator[str]: + return iter(self.__dict__) + + def __repr__(self) -> str: + ctx = _cv_app.get(None) + if ctx is not None: + return f"" + return object.__repr__(self) + + +def after_this_request( + f: ft.AfterRequestCallable[t.Any], +) -> ft.AfterRequestCallable[t.Any]: + """Executes a function after this request. This is useful to modify + response objects. The function is passed the response object and has + to return the same or a new one. + + Example:: + + @app.route('/') + def index(): + @after_this_request + def add_header(response): + response.headers['X-Foo'] = 'Parachute' + return response + return 'Hello World!' + + This is more useful if a function other than the view function wants to + modify a response. For instance think of a decorator that wants to add + some headers without converting the return value into a response object. + + .. versionadded:: 0.9 + """ + ctx = _cv_request.get(None) + + if ctx is None: + raise RuntimeError( + "'after_this_request' can only be used when a request" + " context is active, such as in a view function." + ) + + ctx._after_request_functions.append(f) + return f + + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + + +def copy_current_request_context(f: F) -> F: + """A helper function that decorates a function to retain the current + request context. This is useful when working with greenlets. The moment + the function is decorated a copy of the request context is created and + then pushed when the function is called. The current session is also + included in the copied request context. + + Example:: + + import gevent + from flask import copy_current_request_context + + @app.route('/') + def index(): + @copy_current_request_context + def do_some_work(): + # do some work here, it can access flask.request or + # flask.session like you would otherwise in the view function. + ... + gevent.spawn(do_some_work) + return 'Regular response' + + .. versionadded:: 0.10 + """ + ctx = _cv_request.get(None) + + if ctx is None: + raise RuntimeError( + "'copy_current_request_context' can only be used when a" + " request context is active, such as in a view function." + ) + + ctx = ctx.copy() + + def wrapper(*args: t.Any, **kwargs: t.Any) -> t.Any: + with ctx: # type: ignore[union-attr] + return ctx.app.ensure_sync(f)(*args, **kwargs) # type: ignore[union-attr] + + return update_wrapper(wrapper, f) # type: ignore[return-value] + + +def has_request_context() -> bool: + """If you have code that wants to test if a request context is there or + not this function can be used. For instance, you may want to take advantage + of request information if the request object is available, but fail + silently if it is unavailable. + + :: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and has_request_context(): + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + Alternatively you can also just test any of the context bound objects + (such as :class:`request` or :class:`g`) for truthness:: + + class User(db.Model): + + def __init__(self, username, remote_addr=None): + self.username = username + if remote_addr is None and request: + remote_addr = request.remote_addr + self.remote_addr = remote_addr + + .. versionadded:: 0.7 + """ + return _cv_request.get(None) is not None + + +def has_app_context() -> bool: + """Works like :func:`has_request_context` but for the application + context. You can also just do a boolean check on the + :data:`current_app` object instead. + + .. versionadded:: 0.9 + """ + return _cv_app.get(None) is not None + + +class AppContext: + """The app context contains application-specific information. An app + context is created and pushed at the beginning of each request if + one is not already active. An app context is also pushed when + running CLI commands. + """ + + def __init__(self, app: Flask) -> None: + self.app = app + self.url_adapter = app.create_url_adapter(None) + self.g: _AppCtxGlobals = app.app_ctx_globals_class() + self._cv_tokens: list[contextvars.Token[AppContext]] = [] + + def push(self) -> None: + """Binds the app context to the current context.""" + self._cv_tokens.append(_cv_app.set(self)) + appcontext_pushed.send(self.app, _async_wrapper=self.app.ensure_sync) + + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore + """Pops the app context.""" + try: + if len(self._cv_tokens) == 1: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_appcontext(exc) + finally: + ctx = _cv_app.get() + _cv_app.reset(self._cv_tokens.pop()) + + if ctx is not self: + raise AssertionError( + f"Popped wrong app context. ({ctx!r} instead of {self!r})" + ) + + appcontext_popped.send(self.app, _async_wrapper=self.app.ensure_sync) + + def __enter__(self) -> AppContext: + self.push() + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.pop(exc_value) + + +class RequestContext: + """The request context contains per-request information. The Flask + app creates and pushes it at the beginning of the request, then pops + it at the end of the request. It will create the URL adapter and + request object for the WSGI environment provided. + + Do not attempt to use this class directly, instead use + :meth:`~flask.Flask.test_request_context` and + :meth:`~flask.Flask.request_context` to create this object. + + When the request context is popped, it will evaluate all the + functions registered on the application for teardown execution + (:meth:`~flask.Flask.teardown_request`). + + The request context is automatically popped at the end of the + request. When using the interactive debugger, the context will be + restored so ``request`` is still accessible. Similarly, the test + client can preserve the context after the request ends. However, + teardown functions may already have closed some resources such as + database connections. + """ + + def __init__( + self, + app: Flask, + environ: WSGIEnvironment, + request: Request | None = None, + session: SessionMixin | None = None, + ) -> None: + self.app = app + if request is None: + request = app.request_class(environ) + request.json_module = app.json + self.request: Request = request + self.url_adapter = None + try: + self.url_adapter = app.create_url_adapter(self.request) + except HTTPException as e: + self.request.routing_exception = e + self.flashes: list[tuple[str, str]] | None = None + self.session: SessionMixin | None = session + # Functions that should be executed after the request on the response + # object. These will be called before the regular "after_request" + # functions. + self._after_request_functions: list[ft.AfterRequestCallable[t.Any]] = [] + + self._cv_tokens: list[ + tuple[contextvars.Token[RequestContext], AppContext | None] + ] = [] + + def copy(self) -> RequestContext: + """Creates a copy of this request context with the same request object. + This can be used to move a request context to a different greenlet. + Because the actual request object is the same this cannot be used to + move a request context to a different thread unless access to the + request object is locked. + + .. versionadded:: 0.10 + + .. versionchanged:: 1.1 + The current session object is used instead of reloading the original + data. This prevents `flask.session` pointing to an out-of-date object. + """ + return self.__class__( + self.app, + environ=self.request.environ, + request=self.request, + session=self.session, + ) + + def match_request(self) -> None: + """Can be overridden by a subclass to hook into the matching + of the request. + """ + try: + result = self.url_adapter.match(return_rule=True) # type: ignore + self.request.url_rule, self.request.view_args = result # type: ignore + except HTTPException as e: + self.request.routing_exception = e + + def push(self) -> None: + # Before we push the request context we have to ensure that there + # is an application context. + app_ctx = _cv_app.get(None) + + if app_ctx is None or app_ctx.app is not self.app: + app_ctx = self.app.app_context() + app_ctx.push() + else: + app_ctx = None + + self._cv_tokens.append((_cv_request.set(self), app_ctx)) + + # Open the session at the moment that the request context is available. + # This allows a custom open_session method to use the request context. + # Only open a new session if this is the first time the request was + # pushed, otherwise stream_with_context loses the session. + if self.session is None: + session_interface = self.app.session_interface + self.session = session_interface.open_session(self.app, self.request) + + if self.session is None: + self.session = session_interface.make_null_session(self.app) + + # Match the request URL after loading the session, so that the + # session is available in custom URL converters. + if self.url_adapter is not None: + self.match_request() + + def pop(self, exc: BaseException | None = _sentinel) -> None: # type: ignore + """Pops the request context and unbinds it by doing that. This will + also trigger the execution of functions registered by the + :meth:`~flask.Flask.teardown_request` decorator. + + .. versionchanged:: 0.9 + Added the `exc` argument. + """ + clear_request = len(self._cv_tokens) == 1 + + try: + if clear_request: + if exc is _sentinel: + exc = sys.exc_info()[1] + self.app.do_teardown_request(exc) + + request_close = getattr(self.request, "close", None) + if request_close is not None: + request_close() + finally: + ctx = _cv_request.get() + token, app_ctx = self._cv_tokens.pop() + _cv_request.reset(token) + + # get rid of circular dependencies at the end of the request + # so that we don't require the GC to be active. + if clear_request: + ctx.request.environ["werkzeug.request"] = None + + if app_ctx is not None: + app_ctx.pop(exc) + + if ctx is not self: + raise AssertionError( + f"Popped wrong request context. ({ctx!r} instead of {self!r})" + ) + + def __enter__(self) -> RequestContext: + self.push() + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.pop(exc_value) + + def __repr__(self) -> str: + return ( + f"<{type(self).__name__} {self.request.url!r}" + f" [{self.request.method}] of {self.app.name}>" + ) diff --git a/netdeploy/lib/python3.11/site-packages/flask/debughelpers.py b/netdeploy/lib/python3.11/site-packages/flask/debughelpers.py new file mode 100644 index 0000000..2c8c4c4 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/debughelpers.py @@ -0,0 +1,178 @@ +from __future__ import annotations + +import typing as t + +from jinja2.loaders import BaseLoader +from werkzeug.routing import RequestRedirect + +from .blueprints import Blueprint +from .globals import request_ctx +from .sansio.app import App + +if t.TYPE_CHECKING: + from .sansio.scaffold import Scaffold + from .wrappers import Request + + +class UnexpectedUnicodeError(AssertionError, UnicodeError): + """Raised in places where we want some better error reporting for + unexpected unicode or binary data. + """ + + +class DebugFilesKeyError(KeyError, AssertionError): + """Raised from request.files during debugging. The idea is that it can + provide a better error message than just a generic KeyError/BadRequest. + """ + + def __init__(self, request: Request, key: str) -> None: + form_matches = request.form.getlist(key) + buf = [ + f"You tried to access the file {key!r} in the request.files" + " dictionary but it does not exist. The mimetype for the" + f" request is {request.mimetype!r} instead of" + " 'multipart/form-data' which means that no file contents" + " were transmitted. To fix this error you should provide" + ' enctype="multipart/form-data" in your form.' + ] + if form_matches: + names = ", ".join(repr(x) for x in form_matches) + buf.append( + "\n\nThe browser instead transmitted some file names. " + f"This was submitted: {names}" + ) + self.msg = "".join(buf) + + def __str__(self) -> str: + return self.msg + + +class FormDataRoutingRedirect(AssertionError): + """This exception is raised in debug mode if a routing redirect + would cause the browser to drop the method or body. This happens + when method is not GET, HEAD or OPTIONS and the status code is not + 307 or 308. + """ + + def __init__(self, request: Request) -> None: + exc = request.routing_exception + assert isinstance(exc, RequestRedirect) + buf = [ + f"A request was sent to '{request.url}', but routing issued" + f" a redirect to the canonical URL '{exc.new_url}'." + ] + + if f"{request.base_url}/" == exc.new_url.partition("?")[0]: + buf.append( + " The URL was defined with a trailing slash. Flask" + " will redirect to the URL with a trailing slash if it" + " was accessed without one." + ) + + buf.append( + " Send requests to the canonical URL, or use 307 or 308 for" + " routing redirects. Otherwise, browsers will drop form" + " data.\n\n" + "This exception is only raised in debug mode." + ) + super().__init__("".join(buf)) + + +def attach_enctype_error_multidict(request: Request) -> None: + """Patch ``request.files.__getitem__`` to raise a descriptive error + about ``enctype=multipart/form-data``. + + :param request: The request to patch. + :meta private: + """ + oldcls = request.files.__class__ + + class newcls(oldcls): # type: ignore[valid-type, misc] + def __getitem__(self, key: str) -> t.Any: + try: + return super().__getitem__(key) + except KeyError as e: + if key not in request.form: + raise + + raise DebugFilesKeyError(request, key).with_traceback( + e.__traceback__ + ) from None + + newcls.__name__ = oldcls.__name__ + newcls.__module__ = oldcls.__module__ + request.files.__class__ = newcls + + +def _dump_loader_info(loader: BaseLoader) -> t.Iterator[str]: + yield f"class: {type(loader).__module__}.{type(loader).__name__}" + for key, value in sorted(loader.__dict__.items()): + if key.startswith("_"): + continue + if isinstance(value, (tuple, list)): + if not all(isinstance(x, str) for x in value): + continue + yield f"{key}:" + for item in value: + yield f" - {item}" + continue + elif not isinstance(value, (str, int, float, bool)): + continue + yield f"{key}: {value!r}" + + +def explain_template_loading_attempts( + app: App, + template: str, + attempts: list[ + tuple[ + BaseLoader, + Scaffold, + tuple[str, str | None, t.Callable[[], bool] | None] | None, + ] + ], +) -> None: + """This should help developers understand what failed""" + info = [f"Locating template {template!r}:"] + total_found = 0 + blueprint = None + if request_ctx and request_ctx.request.blueprint is not None: + blueprint = request_ctx.request.blueprint + + for idx, (loader, srcobj, triple) in enumerate(attempts): + if isinstance(srcobj, App): + src_info = f"application {srcobj.import_name!r}" + elif isinstance(srcobj, Blueprint): + src_info = f"blueprint {srcobj.name!r} ({srcobj.import_name})" + else: + src_info = repr(srcobj) + + info.append(f"{idx + 1:5}: trying loader of {src_info}") + + for line in _dump_loader_info(loader): + info.append(f" {line}") + + if triple is None: + detail = "no match" + else: + detail = f"found ({triple[1] or ''!r})" + total_found += 1 + info.append(f" -> {detail}") + + seems_fishy = False + if total_found == 0: + info.append("Error: the template could not be found.") + seems_fishy = True + elif total_found > 1: + info.append("Warning: multiple loaders returned a match for the template.") + seems_fishy = True + + if blueprint is not None and seems_fishy: + info.append( + " The template was looked up from an endpoint that belongs" + f" to the blueprint {blueprint!r}." + ) + info.append(" Maybe you did not place a template in the right folder?") + info.append(" See https://flask.palletsprojects.com/blueprints/#templates") + + app.logger.info("\n".join(info)) diff --git a/netdeploy/lib/python3.11/site-packages/flask/globals.py b/netdeploy/lib/python3.11/site-packages/flask/globals.py new file mode 100644 index 0000000..e2c410c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/globals.py @@ -0,0 +1,51 @@ +from __future__ import annotations + +import typing as t +from contextvars import ContextVar + +from werkzeug.local import LocalProxy + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + from .ctx import _AppCtxGlobals + from .ctx import AppContext + from .ctx import RequestContext + from .sessions import SessionMixin + from .wrappers import Request + + +_no_app_msg = """\ +Working outside of application context. + +This typically means that you attempted to use functionality that needed +the current application. To solve this, set up an application context +with app.app_context(). See the documentation for more information.\ +""" +_cv_app: ContextVar[AppContext] = ContextVar("flask.app_ctx") +app_ctx: AppContext = LocalProxy( # type: ignore[assignment] + _cv_app, unbound_message=_no_app_msg +) +current_app: Flask = LocalProxy( # type: ignore[assignment] + _cv_app, "app", unbound_message=_no_app_msg +) +g: _AppCtxGlobals = LocalProxy( # type: ignore[assignment] + _cv_app, "g", unbound_message=_no_app_msg +) + +_no_req_msg = """\ +Working outside of request context. + +This typically means that you attempted to use functionality that needed +an active HTTP request. Consult the documentation on testing for +information about how to avoid this problem.\ +""" +_cv_request: ContextVar[RequestContext] = ContextVar("flask.request_ctx") +request_ctx: RequestContext = LocalProxy( # type: ignore[assignment] + _cv_request, unbound_message=_no_req_msg +) +request: Request = LocalProxy( # type: ignore[assignment] + _cv_request, "request", unbound_message=_no_req_msg +) +session: SessionMixin = LocalProxy( # type: ignore[assignment] + _cv_request, "session", unbound_message=_no_req_msg +) diff --git a/netdeploy/lib/python3.11/site-packages/flask/helpers.py b/netdeploy/lib/python3.11/site-packages/flask/helpers.py new file mode 100644 index 0000000..359a842 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/helpers.py @@ -0,0 +1,621 @@ +from __future__ import annotations + +import importlib.util +import os +import sys +import typing as t +from datetime import datetime +from functools import lru_cache +from functools import update_wrapper + +import werkzeug.utils +from werkzeug.exceptions import abort as _wz_abort +from werkzeug.utils import redirect as _wz_redirect +from werkzeug.wrappers import Response as BaseResponse + +from .globals import _cv_request +from .globals import current_app +from .globals import request +from .globals import request_ctx +from .globals import session +from .signals import message_flashed + +if t.TYPE_CHECKING: # pragma: no cover + from .wrappers import Response + + +def get_debug_flag() -> bool: + """Get whether debug mode should be enabled for the app, indicated by the + :envvar:`FLASK_DEBUG` environment variable. The default is ``False``. + """ + val = os.environ.get("FLASK_DEBUG") + return bool(val and val.lower() not in {"0", "false", "no"}) + + +def get_load_dotenv(default: bool = True) -> bool: + """Get whether the user has disabled loading default dotenv files by + setting :envvar:`FLASK_SKIP_DOTENV`. The default is ``True``, load + the files. + + :param default: What to return if the env var isn't set. + """ + val = os.environ.get("FLASK_SKIP_DOTENV") + + if not val: + return default + + return val.lower() in ("0", "false", "no") + + +def stream_with_context( + generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]], +) -> t.Iterator[t.AnyStr]: + """Request contexts disappear when the response is started on the server. + This is done for efficiency reasons and to make it less likely to encounter + memory leaks with badly written WSGI middlewares. The downside is that if + you are using streamed responses, the generator cannot access request bound + information any more. + + This function however can help you keep the context around for longer:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + @stream_with_context + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(generate()) + + Alternatively it can also be used around a specific generator:: + + from flask import stream_with_context, request, Response + + @app.route('/stream') + def streamed_response(): + def generate(): + yield 'Hello ' + yield request.args['name'] + yield '!' + return Response(stream_with_context(generate())) + + .. versionadded:: 0.9 + """ + try: + gen = iter(generator_or_function) # type: ignore[arg-type] + except TypeError: + + def decorator(*args: t.Any, **kwargs: t.Any) -> t.Any: + gen = generator_or_function(*args, **kwargs) # type: ignore[operator] + return stream_with_context(gen) + + return update_wrapper(decorator, generator_or_function) # type: ignore[arg-type] + + def generator() -> t.Iterator[t.AnyStr | None]: + ctx = _cv_request.get(None) + if ctx is None: + raise RuntimeError( + "'stream_with_context' can only be used when a request" + " context is active, such as in a view function." + ) + with ctx: + # Dummy sentinel. Has to be inside the context block or we're + # not actually keeping the context around. + yield None + + # The try/finally is here so that if someone passes a WSGI level + # iterator in we're still running the cleanup logic. Generators + # don't need that because they are closed on their destruction + # automatically. + try: + yield from gen + finally: + if hasattr(gen, "close"): + gen.close() + + # The trick is to start the generator. Then the code execution runs until + # the first dummy None is yielded at which point the context was already + # pushed. This item is discarded. Then when the iteration continues the + # real generator is executed. + wrapped_g = generator() + next(wrapped_g) + return wrapped_g # type: ignore[return-value] + + +def make_response(*args: t.Any) -> Response: + """Sometimes it is necessary to set additional headers in a view. Because + views do not have to return response objects but can return a value that + is converted into a response object by Flask itself, it becomes tricky to + add headers to it. This function can be called instead of using a return + and you will get a response object which you can use to attach headers. + + If view looked like this and you want to add a new header:: + + def index(): + return render_template('index.html', foo=42) + + You can now do something like this:: + + def index(): + response = make_response(render_template('index.html', foo=42)) + response.headers['X-Parachutes'] = 'parachutes are cool' + return response + + This function accepts the very same arguments you can return from a + view function. This for example creates a response with a 404 error + code:: + + response = make_response(render_template('not_found.html'), 404) + + The other use case of this function is to force the return value of a + view function into a response which is helpful with view + decorators:: + + response = make_response(view_function()) + response.headers['X-Parachutes'] = 'parachutes are cool' + + Internally this function does the following things: + + - if no arguments are passed, it creates a new response argument + - if one argument is passed, :meth:`flask.Flask.make_response` + is invoked with it. + - if more than one argument is passed, the arguments are passed + to the :meth:`flask.Flask.make_response` function as tuple. + + .. versionadded:: 0.6 + """ + if not args: + return current_app.response_class() + if len(args) == 1: + args = args[0] + return current_app.make_response(args) + + +def url_for( + endpoint: str, + *, + _anchor: str | None = None, + _method: str | None = None, + _scheme: str | None = None, + _external: bool | None = None, + **values: t.Any, +) -> str: + """Generate a URL to the given endpoint with the given values. + + This requires an active request or application context, and calls + :meth:`current_app.url_for() `. See that method + for full documentation. + + :param endpoint: The endpoint name associated with the URL to + generate. If this starts with a ``.``, the current blueprint + name (if any) will be used. + :param _anchor: If given, append this as ``#anchor`` to the URL. + :param _method: If given, generate the URL associated with this + method for the endpoint. + :param _scheme: If given, the URL will have this scheme if it is + external. + :param _external: If given, prefer the URL to be internal (False) or + require it to be external (True). External URLs include the + scheme and domain. When not in an active request, URLs are + external by default. + :param values: Values to use for the variable parts of the URL rule. + Unknown keys are appended as query string arguments, like + ``?a=b&c=d``. + + .. versionchanged:: 2.2 + Calls ``current_app.url_for``, allowing an app to override the + behavior. + + .. versionchanged:: 0.10 + The ``_scheme`` parameter was added. + + .. versionchanged:: 0.9 + The ``_anchor`` and ``_method`` parameters were added. + + .. versionchanged:: 0.9 + Calls ``app.handle_url_build_error`` on build errors. + """ + return current_app.url_for( + endpoint, + _anchor=_anchor, + _method=_method, + _scheme=_scheme, + _external=_external, + **values, + ) + + +def redirect( + location: str, code: int = 302, Response: type[BaseResponse] | None = None +) -> BaseResponse: + """Create a redirect response object. + + If :data:`~flask.current_app` is available, it will use its + :meth:`~flask.Flask.redirect` method, otherwise it will use + :func:`werkzeug.utils.redirect`. + + :param location: The URL to redirect to. + :param code: The status code for the redirect. + :param Response: The response class to use. Not used when + ``current_app`` is active, which uses ``app.response_class``. + + .. versionadded:: 2.2 + Calls ``current_app.redirect`` if available instead of always + using Werkzeug's default ``redirect``. + """ + if current_app: + return current_app.redirect(location, code=code) + + return _wz_redirect(location, code=code, Response=Response) + + +def abort(code: int | BaseResponse, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: + """Raise an :exc:`~werkzeug.exceptions.HTTPException` for the given + status code. + + If :data:`~flask.current_app` is available, it will call its + :attr:`~flask.Flask.aborter` object, otherwise it will use + :func:`werkzeug.exceptions.abort`. + + :param code: The status code for the exception, which must be + registered in ``app.aborter``. + :param args: Passed to the exception. + :param kwargs: Passed to the exception. + + .. versionadded:: 2.2 + Calls ``current_app.aborter`` if available instead of always + using Werkzeug's default ``abort``. + """ + if current_app: + current_app.aborter(code, *args, **kwargs) + + _wz_abort(code, *args, **kwargs) + + +def get_template_attribute(template_name: str, attribute: str) -> t.Any: + """Loads a macro (or variable) a template exports. This can be used to + invoke a macro from within Python code. If you for example have a + template named :file:`_cider.html` with the following contents: + + .. sourcecode:: html+jinja + + {% macro hello(name) %}Hello {{ name }}!{% endmacro %} + + You can access this from Python code like this:: + + hello = get_template_attribute('_cider.html', 'hello') + return hello('World') + + .. versionadded:: 0.2 + + :param template_name: the name of the template + :param attribute: the name of the variable of macro to access + """ + return getattr(current_app.jinja_env.get_template(template_name).module, attribute) + + +def flash(message: str, category: str = "message") -> None: + """Flashes a message to the next request. In order to remove the + flashed message from the session and to display it to the user, + the template has to call :func:`get_flashed_messages`. + + .. versionchanged:: 0.3 + `category` parameter added. + + :param message: the message to be flashed. + :param category: the category for the message. The following values + are recommended: ``'message'`` for any kind of message, + ``'error'`` for errors, ``'info'`` for information + messages and ``'warning'`` for warnings. However any + kind of string can be used as category. + """ + # Original implementation: + # + # session.setdefault('_flashes', []).append((category, message)) + # + # This assumed that changes made to mutable structures in the session are + # always in sync with the session object, which is not true for session + # implementations that use external storage for keeping their keys/values. + flashes = session.get("_flashes", []) + flashes.append((category, message)) + session["_flashes"] = flashes + app = current_app._get_current_object() # type: ignore + message_flashed.send( + app, + _async_wrapper=app.ensure_sync, + message=message, + category=category, + ) + + +def get_flashed_messages( + with_categories: bool = False, category_filter: t.Iterable[str] = () +) -> list[str] | list[tuple[str, str]]: + """Pulls all flashed messages from the session and returns them. + Further calls in the same request to the function will return + the same messages. By default just the messages are returned, + but when `with_categories` is set to ``True``, the return value will + be a list of tuples in the form ``(category, message)`` instead. + + Filter the flashed messages to one or more categories by providing those + categories in `category_filter`. This allows rendering categories in + separate html blocks. The `with_categories` and `category_filter` + arguments are distinct: + + * `with_categories` controls whether categories are returned with message + text (``True`` gives a tuple, where ``False`` gives just the message text). + * `category_filter` filters the messages down to only those matching the + provided categories. + + See :doc:`/patterns/flashing` for examples. + + .. versionchanged:: 0.3 + `with_categories` parameter added. + + .. versionchanged:: 0.9 + `category_filter` parameter added. + + :param with_categories: set to ``True`` to also receive categories. + :param category_filter: filter of categories to limit return values. Only + categories in the list will be returned. + """ + flashes = request_ctx.flashes + if flashes is None: + flashes = session.pop("_flashes") if "_flashes" in session else [] + request_ctx.flashes = flashes + if category_filter: + flashes = list(filter(lambda f: f[0] in category_filter, flashes)) + if not with_categories: + return [x[1] for x in flashes] + return flashes + + +def _prepare_send_file_kwargs(**kwargs: t.Any) -> dict[str, t.Any]: + if kwargs.get("max_age") is None: + kwargs["max_age"] = current_app.get_send_file_max_age + + kwargs.update( + environ=request.environ, + use_x_sendfile=current_app.config["USE_X_SENDFILE"], + response_class=current_app.response_class, + _root_path=current_app.root_path, # type: ignore + ) + return kwargs + + +def send_file( + path_or_file: os.PathLike[t.AnyStr] | str | t.BinaryIO, + mimetype: str | None = None, + as_attachment: bool = False, + download_name: str | None = None, + conditional: bool = True, + etag: bool | str = True, + last_modified: datetime | int | float | None = None, + max_age: None | (int | t.Callable[[str | None], int | None]) = None, +) -> Response: + """Send the contents of a file to the client. + + The first argument can be a file path or a file-like object. Paths + are preferred in most cases because Werkzeug can manage the file and + get extra information from the path. Passing a file-like object + requires that the file is opened in binary mode, and is mostly + useful when building a file in memory with :class:`io.BytesIO`. + + Never pass file paths provided by a user. The path is assumed to be + trusted, so a user could craft a path to access a file you didn't + intend. Use :func:`send_from_directory` to safely serve + user-requested paths from within a directory. + + If the WSGI server sets a ``file_wrapper`` in ``environ``, it is + used, otherwise Werkzeug's built-in wrapper is used. Alternatively, + if the HTTP server supports ``X-Sendfile``, configuring Flask with + ``USE_X_SENDFILE = True`` will tell the server to send the given + path, which is much more efficient than reading it in Python. + + :param path_or_file: The path to the file to send, relative to the + current working directory if a relative path is given. + Alternatively, a file-like object opened in binary mode. Make + sure the file pointer is seeked to the start of the data. + :param mimetype: The MIME type to send for the file. If not + provided, it will try to detect it from the file name. + :param as_attachment: Indicate to a browser that it should offer to + save the file instead of displaying it. + :param download_name: The default name browsers will use when saving + the file. Defaults to the passed file name. + :param conditional: Enable conditional and range responses based on + request headers. Requires passing a file path and ``environ``. + :param etag: Calculate an ETag for the file, which requires passing + a file path. Can also be a string to use instead. + :param last_modified: The last modified time to send for the file, + in seconds. If not provided, it will try to detect it from the + file path. + :param max_age: How long the client should cache the file, in + seconds. If set, ``Cache-Control`` will be ``public``, otherwise + it will be ``no-cache`` to prefer conditional caching. + + .. versionchanged:: 2.0 + ``download_name`` replaces the ``attachment_filename`` + parameter. If ``as_attachment=False``, it is passed with + ``Content-Disposition: inline`` instead. + + .. versionchanged:: 2.0 + ``max_age`` replaces the ``cache_timeout`` parameter. + ``conditional`` is enabled and ``max_age`` is not set by + default. + + .. versionchanged:: 2.0 + ``etag`` replaces the ``add_etags`` parameter. It can be a + string to use instead of generating one. + + .. versionchanged:: 2.0 + Passing a file-like object that inherits from + :class:`~io.TextIOBase` will raise a :exc:`ValueError` rather + than sending an empty file. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. + + .. versionchanged:: 1.1 + ``filename`` may be a :class:`~os.PathLike` object. + + .. versionchanged:: 1.1 + Passing a :class:`~io.BytesIO` object supports range requests. + + .. versionchanged:: 1.0.3 + Filenames are encoded with ASCII instead of Latin-1 for broader + compatibility with WSGI servers. + + .. versionchanged:: 1.0 + UTF-8 filenames as specified in :rfc:`2231` are supported. + + .. versionchanged:: 0.12 + The filename is no longer automatically inferred from file + objects. If you want to use automatic MIME and etag support, + pass a filename via ``filename_or_fp`` or + ``attachment_filename``. + + .. versionchanged:: 0.12 + ``attachment_filename`` is preferred over ``filename`` for MIME + detection. + + .. versionchanged:: 0.9 + ``cache_timeout`` defaults to + :meth:`Flask.get_send_file_max_age`. + + .. versionchanged:: 0.7 + MIME guessing and etag support for file-like objects was + removed because it was unreliable. Pass a filename if you are + able to, otherwise attach an etag yourself. + + .. versionchanged:: 0.5 + The ``add_etags``, ``cache_timeout`` and ``conditional`` + parameters were added. The default behavior is to add etags. + + .. versionadded:: 0.2 + """ + return werkzeug.utils.send_file( # type: ignore[return-value] + **_prepare_send_file_kwargs( + path_or_file=path_or_file, + environ=request.environ, + mimetype=mimetype, + as_attachment=as_attachment, + download_name=download_name, + conditional=conditional, + etag=etag, + last_modified=last_modified, + max_age=max_age, + ) + ) + + +def send_from_directory( + directory: os.PathLike[str] | str, + path: os.PathLike[str] | str, + **kwargs: t.Any, +) -> Response: + """Send a file from within a directory using :func:`send_file`. + + .. code-block:: python + + @app.route("/uploads/") + def download_file(name): + return send_from_directory( + app.config['UPLOAD_FOLDER'], name, as_attachment=True + ) + + This is a secure way to serve files from a folder, such as static + files or uploads. Uses :func:`~werkzeug.security.safe_join` to + ensure the path coming from the client is not maliciously crafted to + point outside the specified directory. + + If the final path does not point to an existing regular file, + raises a 404 :exc:`~werkzeug.exceptions.NotFound` error. + + :param directory: The directory that ``path`` must be located under, + relative to the current application's root path. + :param path: The path to the file to send, relative to + ``directory``. + :param kwargs: Arguments to pass to :func:`send_file`. + + .. versionchanged:: 2.0 + ``path`` replaces the ``filename`` parameter. + + .. versionadded:: 2.0 + Moved the implementation to Werkzeug. This is now a wrapper to + pass some Flask-specific arguments. + + .. versionadded:: 0.5 + """ + return werkzeug.utils.send_from_directory( # type: ignore[return-value] + directory, path, **_prepare_send_file_kwargs(**kwargs) + ) + + +def get_root_path(import_name: str) -> str: + """Find the root path of a package, or the path that contains a + module. If it cannot be found, returns the current working + directory. + + Not to be confused with the value returned by :func:`find_package`. + + :meta private: + """ + # Module already imported and has a file attribute. Use that first. + mod = sys.modules.get(import_name) + + if mod is not None and hasattr(mod, "__file__") and mod.__file__ is not None: + return os.path.dirname(os.path.abspath(mod.__file__)) + + # Next attempt: check the loader. + try: + spec = importlib.util.find_spec(import_name) + + if spec is None: + raise ValueError + except (ImportError, ValueError): + loader = None + else: + loader = spec.loader + + # Loader does not exist or we're referring to an unloaded main + # module or a main module without path (interactive sessions), go + # with the current working directory. + if loader is None: + return os.getcwd() + + if hasattr(loader, "get_filename"): + filepath = loader.get_filename(import_name) + else: + # Fall back to imports. + __import__(import_name) + mod = sys.modules[import_name] + filepath = getattr(mod, "__file__", None) + + # If we don't have a file path it might be because it is a + # namespace package. In this case pick the root path from the + # first module that is contained in the package. + if filepath is None: + raise RuntimeError( + "No root path can be found for the provided module" + f" {import_name!r}. This can happen because the module" + " came from an import hook that does not provide file" + " name information or because it's a namespace package." + " In this case the root path needs to be explicitly" + " provided." + ) + + # filepath is import_name.py for a module, or __init__.py for a package. + return os.path.dirname(os.path.abspath(filepath)) # type: ignore[no-any-return] + + +@lru_cache(maxsize=None) +def _split_blueprint_path(name: str) -> list[str]: + out: list[str] = [name] + + if "." in name: + out.extend(_split_blueprint_path(name.rpartition(".")[0])) + + return out diff --git a/netdeploy/lib/python3.11/site-packages/flask/json/__init__.py b/netdeploy/lib/python3.11/site-packages/flask/json/__init__.py new file mode 100644 index 0000000..c0941d0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/json/__init__.py @@ -0,0 +1,170 @@ +from __future__ import annotations + +import json as _json +import typing as t + +from ..globals import current_app +from .provider import _default + +if t.TYPE_CHECKING: # pragma: no cover + from ..wrappers import Response + + +def dumps(obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.dumps() ` + method, otherwise it will use :func:`json.dumps`. + + :param obj: The data to serialize. + :param kwargs: Arguments passed to the ``dumps`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.dumps``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0.2 + :class:`decimal.Decimal` is supported by converting to a string. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. + + .. versionchanged:: 1.0.3 + ``app`` can be passed directly, rather than requiring an app + context for configuration. + """ + if current_app: + return current_app.json.dumps(obj, **kwargs) + + kwargs.setdefault("default", _default) + return _json.dumps(obj, **kwargs) + + +def dump(obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: + """Serialize data as JSON and write to a file. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.dump() ` + method, otherwise it will use :func:`json.dump`. + + :param obj: The data to serialize. + :param fp: A file opened for writing text. Should use the UTF-8 + encoding to be valid JSON. + :param kwargs: Arguments passed to the ``dump`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.dump``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0 + Writing to a binary file, and the ``encoding`` argument, will be + removed in Flask 2.1. + """ + if current_app: + current_app.json.dump(obj, fp, **kwargs) + else: + kwargs.setdefault("default", _default) + _json.dump(obj, fp, **kwargs) + + +def loads(s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.loads() ` + method, otherwise it will use :func:`json.loads`. + + :param s: Text or UTF-8 bytes. + :param kwargs: Arguments passed to the ``loads`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.loads``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. The data must be a + string or UTF-8 bytes. + + .. versionchanged:: 1.0.3 + ``app`` can be passed directly, rather than requiring an app + context for configuration. + """ + if current_app: + return current_app.json.loads(s, **kwargs) + + return _json.loads(s, **kwargs) + + +def load(fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON read from a file. + + If :data:`~flask.current_app` is available, it will use its + :meth:`app.json.load() ` + method, otherwise it will use :func:`json.load`. + + :param fp: A file opened for reading text or UTF-8 bytes. + :param kwargs: Arguments passed to the ``load`` implementation. + + .. versionchanged:: 2.3 + The ``app`` parameter was removed. + + .. versionchanged:: 2.2 + Calls ``current_app.json.load``, allowing an app to override + the behavior. + + .. versionchanged:: 2.2 + The ``app`` parameter will be removed in Flask 2.3. + + .. versionchanged:: 2.0 + ``encoding`` will be removed in Flask 2.1. The file must be text + mode, or binary mode with UTF-8 bytes. + """ + if current_app: + return current_app.json.load(fp, **kwargs) + + return _json.load(fp, **kwargs) + + +def jsonify(*args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with the ``application/json`` + mimetype. A dict or list returned from a view will be converted to a + JSON response automatically without needing to call this. + + This requires an active request or application context, and calls + :meth:`app.json.response() `. + + In debug mode, the output is formatted with indentation to make it + easier to read. This may also be controlled by the provider. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + + .. versionchanged:: 2.2 + Calls ``current_app.json.response``, allowing an app to override + the behavior. + + .. versionchanged:: 2.0.2 + :class:`decimal.Decimal` is supported by converting to a string. + + .. versionchanged:: 0.11 + Added support for serializing top-level arrays. This was a + security risk in ancient browsers. See :ref:`security-json`. + + .. versionadded:: 0.2 + """ + return current_app.json.response(*args, **kwargs) # type: ignore[return-value] diff --git a/netdeploy/lib/python3.11/site-packages/flask/json/provider.py b/netdeploy/lib/python3.11/site-packages/flask/json/provider.py new file mode 100644 index 0000000..f9b2e8f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/json/provider.py @@ -0,0 +1,215 @@ +from __future__ import annotations + +import dataclasses +import decimal +import json +import typing as t +import uuid +import weakref +from datetime import date + +from werkzeug.http import http_date + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.sansio.response import Response + + from ..sansio.app import App + + +class JSONProvider: + """A standard set of JSON operations for an application. Subclasses + of this can be used to customize JSON behavior or use different + JSON libraries. + + To implement a provider for a specific library, subclass this base + class and implement at least :meth:`dumps` and :meth:`loads`. All + other methods have default implementations. + + To use a different provider, either subclass ``Flask`` and set + :attr:`~flask.Flask.json_provider_class` to a provider class, or set + :attr:`app.json ` to an instance of the class. + + :param app: An application instance. This will be stored as a + :class:`weakref.proxy` on the :attr:`_app` attribute. + + .. versionadded:: 2.2 + """ + + def __init__(self, app: App) -> None: + self._app: App = weakref.proxy(app) + + def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON. + + :param obj: The data to serialize. + :param kwargs: May be passed to the underlying JSON library. + """ + raise NotImplementedError + + def dump(self, obj: t.Any, fp: t.IO[str], **kwargs: t.Any) -> None: + """Serialize data as JSON and write to a file. + + :param obj: The data to serialize. + :param fp: A file opened for writing text. Should use the UTF-8 + encoding to be valid JSON. + :param kwargs: May be passed to the underlying JSON library. + """ + fp.write(self.dumps(obj, **kwargs)) + + def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON. + + :param s: Text or UTF-8 bytes. + :param kwargs: May be passed to the underlying JSON library. + """ + raise NotImplementedError + + def load(self, fp: t.IO[t.AnyStr], **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON read from a file. + + :param fp: A file opened for reading text or UTF-8 bytes. + :param kwargs: May be passed to the underlying JSON library. + """ + return self.loads(fp.read(), **kwargs) + + def _prepare_response_obj( + self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] + ) -> t.Any: + if args and kwargs: + raise TypeError("app.json.response() takes either args or kwargs, not both") + + if not args and not kwargs: + return None + + if len(args) == 1: + return args[0] + + return args or kwargs + + def response(self, *args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with the ``application/json`` + mimetype. + + The :func:`~flask.json.jsonify` function calls this method for + the current application. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + """ + obj = self._prepare_response_obj(args, kwargs) + return self._app.response_class(self.dumps(obj), mimetype="application/json") + + +def _default(o: t.Any) -> t.Any: + if isinstance(o, date): + return http_date(o) + + if isinstance(o, (decimal.Decimal, uuid.UUID)): + return str(o) + + if dataclasses and dataclasses.is_dataclass(o): + return dataclasses.asdict(o) + + if hasattr(o, "__html__"): + return str(o.__html__()) + + raise TypeError(f"Object of type {type(o).__name__} is not JSON serializable") + + +class DefaultJSONProvider(JSONProvider): + """Provide JSON operations using Python's built-in :mod:`json` + library. Serializes the following additional data types: + + - :class:`datetime.datetime` and :class:`datetime.date` are + serialized to :rfc:`822` strings. This is the same as the HTTP + date format. + - :class:`uuid.UUID` is serialized to a string. + - :class:`dataclasses.dataclass` is passed to + :func:`dataclasses.asdict`. + - :class:`~markupsafe.Markup` (or any object with a ``__html__`` + method) will call the ``__html__`` method to get a string. + """ + + default: t.Callable[[t.Any], t.Any] = staticmethod(_default) # type: ignore[assignment] + """Apply this function to any object that :meth:`json.dumps` does + not know how to serialize. It should return a valid JSON type or + raise a ``TypeError``. + """ + + ensure_ascii = True + """Replace non-ASCII characters with escape sequences. This may be + more compatible with some clients, but can be disabled for better + performance and size. + """ + + sort_keys = True + """Sort the keys in any serialized dicts. This may be useful for + some caching situations, but can be disabled for better performance. + When enabled, keys must all be strings, they are not converted + before sorting. + """ + + compact: bool | None = None + """If ``True``, or ``None`` out of debug mode, the :meth:`response` + output will not add indentation, newlines, or spaces. If ``False``, + or ``None`` in debug mode, it will use a non-compact representation. + """ + + mimetype = "application/json" + """The mimetype set in :meth:`response`.""" + + def dumps(self, obj: t.Any, **kwargs: t.Any) -> str: + """Serialize data as JSON to a string. + + Keyword arguments are passed to :func:`json.dumps`. Sets some + parameter defaults from the :attr:`default`, + :attr:`ensure_ascii`, and :attr:`sort_keys` attributes. + + :param obj: The data to serialize. + :param kwargs: Passed to :func:`json.dumps`. + """ + kwargs.setdefault("default", self.default) + kwargs.setdefault("ensure_ascii", self.ensure_ascii) + kwargs.setdefault("sort_keys", self.sort_keys) + return json.dumps(obj, **kwargs) + + def loads(self, s: str | bytes, **kwargs: t.Any) -> t.Any: + """Deserialize data as JSON from a string or bytes. + + :param s: Text or UTF-8 bytes. + :param kwargs: Passed to :func:`json.loads`. + """ + return json.loads(s, **kwargs) + + def response(self, *args: t.Any, **kwargs: t.Any) -> Response: + """Serialize the given arguments as JSON, and return a + :class:`~flask.Response` object with it. The response mimetype + will be "application/json" and can be changed with + :attr:`mimetype`. + + If :attr:`compact` is ``False`` or debug mode is enabled, the + output will be formatted to be easier to read. + + Either positional or keyword arguments can be given, not both. + If no arguments are given, ``None`` is serialized. + + :param args: A single value to serialize, or multiple values to + treat as a list to serialize. + :param kwargs: Treat as a dict to serialize. + """ + obj = self._prepare_response_obj(args, kwargs) + dump_args: dict[str, t.Any] = {} + + if (self.compact is None and self._app.debug) or self.compact is False: + dump_args.setdefault("indent", 2) + else: + dump_args.setdefault("separators", (",", ":")) + + return self._app.response_class( + f"{self.dumps(obj, **dump_args)}\n", mimetype=self.mimetype + ) diff --git a/netdeploy/lib/python3.11/site-packages/flask/json/tag.py b/netdeploy/lib/python3.11/site-packages/flask/json/tag.py new file mode 100644 index 0000000..8dc3629 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/json/tag.py @@ -0,0 +1,327 @@ +""" +Tagged JSON +~~~~~~~~~~~ + +A compact representation for lossless serialization of non-standard JSON +types. :class:`~flask.sessions.SecureCookieSessionInterface` uses this +to serialize the session data, but it may be useful in other places. It +can be extended to support other types. + +.. autoclass:: TaggedJSONSerializer + :members: + +.. autoclass:: JSONTag + :members: + +Let's see an example that adds support for +:class:`~collections.OrderedDict`. Dicts don't have an order in JSON, so +to handle this we will dump the items as a list of ``[key, value]`` +pairs. Subclass :class:`JSONTag` and give it the new key ``' od'`` to +identify the type. The session serializer processes dicts first, so +insert the new tag at the front of the order since ``OrderedDict`` must +be processed before ``dict``. + +.. code-block:: python + + from flask.json.tag import JSONTag + + class TagOrderedDict(JSONTag): + __slots__ = ('serializer',) + key = ' od' + + def check(self, value): + return isinstance(value, OrderedDict) + + def to_json(self, value): + return [[k, self.serializer.tag(v)] for k, v in iteritems(value)] + + def to_python(self, value): + return OrderedDict(value) + + app.session_interface.serializer.register(TagOrderedDict, index=0) +""" + +from __future__ import annotations + +import typing as t +from base64 import b64decode +from base64 import b64encode +from datetime import datetime +from uuid import UUID + +from markupsafe import Markup +from werkzeug.http import http_date +from werkzeug.http import parse_date + +from ..json import dumps +from ..json import loads + + +class JSONTag: + """Base class for defining type tags for :class:`TaggedJSONSerializer`.""" + + __slots__ = ("serializer",) + + #: The tag to mark the serialized object with. If empty, this tag is + #: only used as an intermediate step during tagging. + key: str = "" + + def __init__(self, serializer: TaggedJSONSerializer) -> None: + """Create a tagger for the given serializer.""" + self.serializer = serializer + + def check(self, value: t.Any) -> bool: + """Check if the given value should be tagged by this tag.""" + raise NotImplementedError + + def to_json(self, value: t.Any) -> t.Any: + """Convert the Python object to an object that is a valid JSON type. + The tag will be added later.""" + raise NotImplementedError + + def to_python(self, value: t.Any) -> t.Any: + """Convert the JSON representation back to the correct type. The tag + will already be removed.""" + raise NotImplementedError + + def tag(self, value: t.Any) -> dict[str, t.Any]: + """Convert the value to a valid JSON type and add the tag structure + around it.""" + return {self.key: self.to_json(value)} + + +class TagDict(JSONTag): + """Tag for 1-item dicts whose only key matches a registered tag. + + Internally, the dict key is suffixed with `__`, and the suffix is removed + when deserializing. + """ + + __slots__ = () + key = " di" + + def check(self, value: t.Any) -> bool: + return ( + isinstance(value, dict) + and len(value) == 1 + and next(iter(value)) in self.serializer.tags + ) + + def to_json(self, value: t.Any) -> t.Any: + key = next(iter(value)) + return {f"{key}__": self.serializer.tag(value[key])} + + def to_python(self, value: t.Any) -> t.Any: + key = next(iter(value)) + return {key[:-2]: value[key]} + + +class PassDict(JSONTag): + __slots__ = () + + def check(self, value: t.Any) -> bool: + return isinstance(value, dict) + + def to_json(self, value: t.Any) -> t.Any: + # JSON objects may only have string keys, so don't bother tagging the + # key here. + return {k: self.serializer.tag(v) for k, v in value.items()} + + tag = to_json + + +class TagTuple(JSONTag): + __slots__ = () + key = " t" + + def check(self, value: t.Any) -> bool: + return isinstance(value, tuple) + + def to_json(self, value: t.Any) -> t.Any: + return [self.serializer.tag(item) for item in value] + + def to_python(self, value: t.Any) -> t.Any: + return tuple(value) + + +class PassList(JSONTag): + __slots__ = () + + def check(self, value: t.Any) -> bool: + return isinstance(value, list) + + def to_json(self, value: t.Any) -> t.Any: + return [self.serializer.tag(item) for item in value] + + tag = to_json + + +class TagBytes(JSONTag): + __slots__ = () + key = " b" + + def check(self, value: t.Any) -> bool: + return isinstance(value, bytes) + + def to_json(self, value: t.Any) -> t.Any: + return b64encode(value).decode("ascii") + + def to_python(self, value: t.Any) -> t.Any: + return b64decode(value) + + +class TagMarkup(JSONTag): + """Serialize anything matching the :class:`~markupsafe.Markup` API by + having a ``__html__`` method to the result of that method. Always + deserializes to an instance of :class:`~markupsafe.Markup`.""" + + __slots__ = () + key = " m" + + def check(self, value: t.Any) -> bool: + return callable(getattr(value, "__html__", None)) + + def to_json(self, value: t.Any) -> t.Any: + return str(value.__html__()) + + def to_python(self, value: t.Any) -> t.Any: + return Markup(value) + + +class TagUUID(JSONTag): + __slots__ = () + key = " u" + + def check(self, value: t.Any) -> bool: + return isinstance(value, UUID) + + def to_json(self, value: t.Any) -> t.Any: + return value.hex + + def to_python(self, value: t.Any) -> t.Any: + return UUID(value) + + +class TagDateTime(JSONTag): + __slots__ = () + key = " d" + + def check(self, value: t.Any) -> bool: + return isinstance(value, datetime) + + def to_json(self, value: t.Any) -> t.Any: + return http_date(value) + + def to_python(self, value: t.Any) -> t.Any: + return parse_date(value) + + +class TaggedJSONSerializer: + """Serializer that uses a tag system to compactly represent objects that + are not JSON types. Passed as the intermediate serializer to + :class:`itsdangerous.Serializer`. + + The following extra types are supported: + + * :class:`dict` + * :class:`tuple` + * :class:`bytes` + * :class:`~markupsafe.Markup` + * :class:`~uuid.UUID` + * :class:`~datetime.datetime` + """ + + __slots__ = ("tags", "order") + + #: Tag classes to bind when creating the serializer. Other tags can be + #: added later using :meth:`~register`. + default_tags = [ + TagDict, + PassDict, + TagTuple, + PassList, + TagBytes, + TagMarkup, + TagUUID, + TagDateTime, + ] + + def __init__(self) -> None: + self.tags: dict[str, JSONTag] = {} + self.order: list[JSONTag] = [] + + for cls in self.default_tags: + self.register(cls) + + def register( + self, + tag_class: type[JSONTag], + force: bool = False, + index: int | None = None, + ) -> None: + """Register a new tag with this serializer. + + :param tag_class: tag class to register. Will be instantiated with this + serializer instance. + :param force: overwrite an existing tag. If false (default), a + :exc:`KeyError` is raised. + :param index: index to insert the new tag in the tag order. Useful when + the new tag is a special case of an existing tag. If ``None`` + (default), the tag is appended to the end of the order. + + :raise KeyError: if the tag key is already registered and ``force`` is + not true. + """ + tag = tag_class(self) + key = tag.key + + if key: + if not force and key in self.tags: + raise KeyError(f"Tag '{key}' is already registered.") + + self.tags[key] = tag + + if index is None: + self.order.append(tag) + else: + self.order.insert(index, tag) + + def tag(self, value: t.Any) -> t.Any: + """Convert a value to a tagged representation if necessary.""" + for tag in self.order: + if tag.check(value): + return tag.tag(value) + + return value + + def untag(self, value: dict[str, t.Any]) -> t.Any: + """Convert a tagged representation back to the original type.""" + if len(value) != 1: + return value + + key = next(iter(value)) + + if key not in self.tags: + return value + + return self.tags[key].to_python(value[key]) + + def _untag_scan(self, value: t.Any) -> t.Any: + if isinstance(value, dict): + # untag each item recursively + value = {k: self._untag_scan(v) for k, v in value.items()} + # untag the dict itself + value = self.untag(value) + elif isinstance(value, list): + # untag each item recursively + value = [self._untag_scan(item) for item in value] + + return value + + def dumps(self, value: t.Any) -> str: + """Tag the value and dump it to a compact JSON string.""" + return dumps(self.tag(value), separators=(",", ":")) + + def loads(self, value: str) -> t.Any: + """Load data from a JSON string and deserialized any tagged objects.""" + return self._untag_scan(loads(value)) diff --git a/netdeploy/lib/python3.11/site-packages/flask/logging.py b/netdeploy/lib/python3.11/site-packages/flask/logging.py new file mode 100644 index 0000000..0cb8f43 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/logging.py @@ -0,0 +1,79 @@ +from __future__ import annotations + +import logging +import sys +import typing as t + +from werkzeug.local import LocalProxy + +from .globals import request + +if t.TYPE_CHECKING: # pragma: no cover + from .sansio.app import App + + +@LocalProxy +def wsgi_errors_stream() -> t.TextIO: + """Find the most appropriate error stream for the application. If a request + is active, log to ``wsgi.errors``, otherwise use ``sys.stderr``. + + If you configure your own :class:`logging.StreamHandler`, you may want to + use this for the stream. If you are using file or dict configuration and + can't import this directly, you can refer to it as + ``ext://flask.logging.wsgi_errors_stream``. + """ + if request: + return request.environ["wsgi.errors"] # type: ignore[no-any-return] + + return sys.stderr + + +def has_level_handler(logger: logging.Logger) -> bool: + """Check if there is a handler in the logging chain that will handle the + given logger's :meth:`effective level <~logging.Logger.getEffectiveLevel>`. + """ + level = logger.getEffectiveLevel() + current = logger + + while current: + if any(handler.level <= level for handler in current.handlers): + return True + + if not current.propagate: + break + + current = current.parent # type: ignore + + return False + + +#: Log messages to :func:`~flask.logging.wsgi_errors_stream` with the format +#: ``[%(asctime)s] %(levelname)s in %(module)s: %(message)s``. +default_handler = logging.StreamHandler(wsgi_errors_stream) # type: ignore +default_handler.setFormatter( + logging.Formatter("[%(asctime)s] %(levelname)s in %(module)s: %(message)s") +) + + +def create_logger(app: App) -> logging.Logger: + """Get the Flask app's logger and configure it if needed. + + The logger name will be the same as + :attr:`app.import_name `. + + When :attr:`~flask.Flask.debug` is enabled, set the logger level to + :data:`logging.DEBUG` if it is not set. + + If there is no handler for the logger's effective level, add a + :class:`~logging.StreamHandler` for + :func:`~flask.logging.wsgi_errors_stream` with a basic format. + """ + logger = logging.getLogger(app.name) + + if app.debug and not logger.level: + logger.setLevel(logging.DEBUG) + + if not has_level_handler(logger): + logger.addHandler(default_handler) + + return logger diff --git a/netdeploy/lib/python3.11/site-packages/flask/py.typed b/netdeploy/lib/python3.11/site-packages/flask/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/flask/sansio/README.md b/netdeploy/lib/python3.11/site-packages/flask/sansio/README.md new file mode 100644 index 0000000..623ac19 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/sansio/README.md @@ -0,0 +1,6 @@ +# Sansio + +This folder contains code that can be used by alternative Flask +implementations, for example Quart. The code therefore cannot do any +IO, nor be part of a likely IO path. Finally this code cannot use the +Flask globals. diff --git a/netdeploy/lib/python3.11/site-packages/flask/sansio/app.py b/netdeploy/lib/python3.11/site-packages/flask/sansio/app.py new file mode 100644 index 0000000..01fd5db --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/sansio/app.py @@ -0,0 +1,964 @@ +from __future__ import annotations + +import logging +import os +import sys +import typing as t +from datetime import timedelta +from itertools import chain + +from werkzeug.exceptions import Aborter +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import BadRequestKeyError +from werkzeug.routing import BuildError +from werkzeug.routing import Map +from werkzeug.routing import Rule +from werkzeug.sansio.response import Response +from werkzeug.utils import cached_property +from werkzeug.utils import redirect as _wz_redirect + +from .. import typing as ft +from ..config import Config +from ..config import ConfigAttribute +from ..ctx import _AppCtxGlobals +from ..helpers import _split_blueprint_path +from ..helpers import get_debug_flag +from ..json.provider import DefaultJSONProvider +from ..json.provider import JSONProvider +from ..logging import create_logger +from ..templating import DispatchingJinjaLoader +from ..templating import Environment +from .scaffold import _endpoint_from_view_func +from .scaffold import find_package +from .scaffold import Scaffold +from .scaffold import setupmethod + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.wrappers import Response as BaseResponse + + from ..testing import FlaskClient + from ..testing import FlaskCliRunner + from .blueprints import Blueprint + +T_shell_context_processor = t.TypeVar( + "T_shell_context_processor", bound=ft.ShellContextProcessorCallable +) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) + + +def _make_timedelta(value: timedelta | int | None) -> timedelta | None: + if value is None or isinstance(value, timedelta): + return value + + return timedelta(seconds=value) + + +class App(Scaffold): + """The flask object implements a WSGI application and acts as the central + object. It is passed the name of the module or package of the + application. Once it is created it will act as a central registry for + the view functions, the URL rules, template configuration and much more. + + The name of the package is used to resolve resources from inside the + package or the folder the module is contained in depending on if the + package parameter resolves to an actual python package (a folder with + an :file:`__init__.py` file inside) or a standard module (just a ``.py`` file). + + For more information about resource loading, see :func:`open_resource`. + + Usually you create a :class:`Flask` instance in your main module or + in the :file:`__init__.py` file of your package like this:: + + from flask import Flask + app = Flask(__name__) + + .. admonition:: About the First Parameter + + The idea of the first parameter is to give Flask an idea of what + belongs to your application. This name is used to find resources + on the filesystem, can be used by extensions to improve debugging + information and a lot more. + + So it's important what you provide there. If you are using a single + module, `__name__` is always the correct value. If you however are + using a package, it's usually recommended to hardcode the name of + your package there. + + For example if your application is defined in :file:`yourapplication/app.py` + you should create it with one of the two versions below:: + + app = Flask('yourapplication') + app = Flask(__name__.split('.')[0]) + + Why is that? The application will work even with `__name__`, thanks + to how resources are looked up. However it will make debugging more + painful. Certain extensions can make assumptions based on the + import name of your application. For example the Flask-SQLAlchemy + extension will look for the code in your application that triggered + an SQL query in debug mode. If the import name is not properly set + up, that debugging information is lost. (For example it would only + pick up SQL queries in `yourapplication.app` and not + `yourapplication.views.frontend`) + + .. versionadded:: 0.7 + The `static_url_path`, `static_folder`, and `template_folder` + parameters were added. + + .. versionadded:: 0.8 + The `instance_path` and `instance_relative_config` parameters were + added. + + .. versionadded:: 0.11 + The `root_path` parameter was added. + + .. versionadded:: 1.0 + The ``host_matching`` and ``static_host`` parameters were added. + + .. versionadded:: 1.0 + The ``subdomain_matching`` parameter was added. Subdomain + matching needs to be enabled manually now. Setting + :data:`SERVER_NAME` does not implicitly enable it. + + :param import_name: the name of the application package + :param static_url_path: can be used to specify a different path for the + static files on the web. Defaults to the name + of the `static_folder` folder. + :param static_folder: The folder with static files that is served at + ``static_url_path``. Relative to the application ``root_path`` + or an absolute path. Defaults to ``'static'``. + :param static_host: the host to use when adding the static route. + Defaults to None. Required when using ``host_matching=True`` + with a ``static_folder`` configured. + :param host_matching: set ``url_map.host_matching`` attribute. + Defaults to False. + :param subdomain_matching: consider the subdomain relative to + :data:`SERVER_NAME` when matching routes. Defaults to False. + :param template_folder: the folder that contains the templates that should + be used by the application. Defaults to + ``'templates'`` folder in the root path of the + application. + :param instance_path: An alternative instance path for the application. + By default the folder ``'instance'`` next to the + package or module is assumed to be the instance + path. + :param instance_relative_config: if set to ``True`` relative filenames + for loading the config are assumed to + be relative to the instance path instead + of the application root. + :param root_path: The path to the root of the application files. + This should only be set manually when it can't be detected + automatically, such as for namespace packages. + """ + + #: The class of the object assigned to :attr:`aborter`, created by + #: :meth:`create_aborter`. That object is called by + #: :func:`flask.abort` to raise HTTP errors, and can be + #: called directly as well. + #: + #: Defaults to :class:`werkzeug.exceptions.Aborter`. + #: + #: .. versionadded:: 2.2 + aborter_class = Aborter + + #: The class that is used for the Jinja environment. + #: + #: .. versionadded:: 0.11 + jinja_environment = Environment + + #: The class that is used for the :data:`~flask.g` instance. + #: + #: Example use cases for a custom class: + #: + #: 1. Store arbitrary attributes on flask.g. + #: 2. Add a property for lazy per-request database connectors. + #: 3. Return None instead of AttributeError on unexpected attributes. + #: 4. Raise exception if an unexpected attr is set, a "controlled" flask.g. + #: + #: In Flask 0.9 this property was called `request_globals_class` but it + #: was changed in 0.10 to :attr:`app_ctx_globals_class` because the + #: flask.g object is now application context scoped. + #: + #: .. versionadded:: 0.10 + app_ctx_globals_class = _AppCtxGlobals + + #: The class that is used for the ``config`` attribute of this app. + #: Defaults to :class:`~flask.Config`. + #: + #: Example use cases for a custom class: + #: + #: 1. Default values for certain config options. + #: 2. Access to config values through attributes in addition to keys. + #: + #: .. versionadded:: 0.11 + config_class = Config + + #: The testing flag. Set this to ``True`` to enable the test mode of + #: Flask extensions (and in the future probably also Flask itself). + #: For example this might activate test helpers that have an + #: additional runtime cost which should not be enabled by default. + #: + #: If this is enabled and PROPAGATE_EXCEPTIONS is not changed from the + #: default it's implicitly enabled. + #: + #: This attribute can also be configured from the config with the + #: ``TESTING`` configuration key. Defaults to ``False``. + testing = ConfigAttribute[bool]("TESTING") + + #: If a secret key is set, cryptographic components can use this to + #: sign cookies and other things. Set this to a complex random value + #: when you want to use the secure cookie for instance. + #: + #: This attribute can also be configured from the config with the + #: :data:`SECRET_KEY` configuration key. Defaults to ``None``. + secret_key = ConfigAttribute[t.Union[str, bytes, None]]("SECRET_KEY") + + #: A :class:`~datetime.timedelta` which is used to set the expiration + #: date of a permanent session. The default is 31 days which makes a + #: permanent session survive for roughly one month. + #: + #: This attribute can also be configured from the config with the + #: ``PERMANENT_SESSION_LIFETIME`` configuration key. Defaults to + #: ``timedelta(days=31)`` + permanent_session_lifetime = ConfigAttribute[timedelta]( + "PERMANENT_SESSION_LIFETIME", + get_converter=_make_timedelta, # type: ignore[arg-type] + ) + + json_provider_class: type[JSONProvider] = DefaultJSONProvider + """A subclass of :class:`~flask.json.provider.JSONProvider`. An + instance is created and assigned to :attr:`app.json` when creating + the app. + + The default, :class:`~flask.json.provider.DefaultJSONProvider`, uses + Python's built-in :mod:`json` library. A different provider can use + a different JSON library. + + .. versionadded:: 2.2 + """ + + #: Options that are passed to the Jinja environment in + #: :meth:`create_jinja_environment`. Changing these options after + #: the environment is created (accessing :attr:`jinja_env`) will + #: have no effect. + #: + #: .. versionchanged:: 1.1.0 + #: This is a ``dict`` instead of an ``ImmutableDict`` to allow + #: easier configuration. + #: + jinja_options: dict[str, t.Any] = {} + + #: The rule object to use for URL rules created. This is used by + #: :meth:`add_url_rule`. Defaults to :class:`werkzeug.routing.Rule`. + #: + #: .. versionadded:: 0.7 + url_rule_class = Rule + + #: The map object to use for storing the URL rules and routing + #: configuration parameters. Defaults to :class:`werkzeug.routing.Map`. + #: + #: .. versionadded:: 1.1.0 + url_map_class = Map + + #: The :meth:`test_client` method creates an instance of this test + #: client class. Defaults to :class:`~flask.testing.FlaskClient`. + #: + #: .. versionadded:: 0.7 + test_client_class: type[FlaskClient] | None = None + + #: The :class:`~click.testing.CliRunner` subclass, by default + #: :class:`~flask.testing.FlaskCliRunner` that is used by + #: :meth:`test_cli_runner`. Its ``__init__`` method should take a + #: Flask app object as the first argument. + #: + #: .. versionadded:: 1.0 + test_cli_runner_class: type[FlaskCliRunner] | None = None + + default_config: dict[str, t.Any] + response_class: type[Response] + + def __init__( + self, + import_name: str, + static_url_path: str | None = None, + static_folder: str | os.PathLike[str] | None = "static", + static_host: str | None = None, + host_matching: bool = False, + subdomain_matching: bool = False, + template_folder: str | os.PathLike[str] | None = "templates", + instance_path: str | None = None, + instance_relative_config: bool = False, + root_path: str | None = None, + ): + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, + ) + + if instance_path is None: + instance_path = self.auto_find_instance_path() + elif not os.path.isabs(instance_path): + raise ValueError( + "If an instance path is provided it must be absolute." + " A relative path was given instead." + ) + + #: Holds the path to the instance folder. + #: + #: .. versionadded:: 0.8 + self.instance_path = instance_path + + #: The configuration dictionary as :class:`Config`. This behaves + #: exactly like a regular dictionary but supports additional methods + #: to load a config from files. + self.config = self.make_config(instance_relative_config) + + #: An instance of :attr:`aborter_class` created by + #: :meth:`make_aborter`. This is called by :func:`flask.abort` + #: to raise HTTP errors, and can be called directly as well. + #: + #: .. versionadded:: 2.2 + #: Moved from ``flask.abort``, which calls this object. + self.aborter = self.make_aborter() + + self.json: JSONProvider = self.json_provider_class(self) + """Provides access to JSON methods. Functions in ``flask.json`` + will call methods on this provider when the application context + is active. Used for handling JSON requests and responses. + + An instance of :attr:`json_provider_class`. Can be customized by + changing that attribute on a subclass, or by assigning to this + attribute afterwards. + + The default, :class:`~flask.json.provider.DefaultJSONProvider`, + uses Python's built-in :mod:`json` library. A different provider + can use a different JSON library. + + .. versionadded:: 2.2 + """ + + #: A list of functions that are called by + #: :meth:`handle_url_build_error` when :meth:`.url_for` raises a + #: :exc:`~werkzeug.routing.BuildError`. Each function is called + #: with ``error``, ``endpoint`` and ``values``. If a function + #: returns ``None`` or raises a ``BuildError``, it is skipped. + #: Otherwise, its return value is returned by ``url_for``. + #: + #: .. versionadded:: 0.9 + self.url_build_error_handlers: list[ + t.Callable[[Exception, str, dict[str, t.Any]], str] + ] = [] + + #: A list of functions that are called when the application context + #: is destroyed. Since the application context is also torn down + #: if the request ends this is the place to store code that disconnects + #: from databases. + #: + #: .. versionadded:: 0.9 + self.teardown_appcontext_funcs: list[ft.TeardownCallable] = [] + + #: A list of shell context processor functions that should be run + #: when a shell context is created. + #: + #: .. versionadded:: 0.11 + self.shell_context_processors: list[ft.ShellContextProcessorCallable] = [] + + #: Maps registered blueprint names to blueprint objects. The + #: dict retains the order the blueprints were registered in. + #: Blueprints can be registered multiple times, this dict does + #: not track how often they were attached. + #: + #: .. versionadded:: 0.7 + self.blueprints: dict[str, Blueprint] = {} + + #: a place where extensions can store application specific state. For + #: example this is where an extension could store database engines and + #: similar things. + #: + #: The key must match the name of the extension module. For example in + #: case of a "Flask-Foo" extension in `flask_foo`, the key would be + #: ``'foo'``. + #: + #: .. versionadded:: 0.7 + self.extensions: dict[str, t.Any] = {} + + #: The :class:`~werkzeug.routing.Map` for this instance. You can use + #: this to change the routing converters after the class was created + #: but before any routes are connected. Example:: + #: + #: from werkzeug.routing import BaseConverter + #: + #: class ListConverter(BaseConverter): + #: def to_python(self, value): + #: return value.split(',') + #: def to_url(self, values): + #: return ','.join(super(ListConverter, self).to_url(value) + #: for value in values) + #: + #: app = Flask(__name__) + #: app.url_map.converters['list'] = ListConverter + self.url_map = self.url_map_class(host_matching=host_matching) + + self.subdomain_matching = subdomain_matching + + # tracks internally if the application already handled at least one + # request. + self._got_first_request = False + + def _check_setup_finished(self, f_name: str) -> None: + if self._got_first_request: + raise AssertionError( + f"The setup method '{f_name}' can no longer be called" + " on the application. It has already handled its first" + " request, any changes will not be applied" + " consistently.\n" + "Make sure all imports, decorators, functions, etc." + " needed to set up the application are done before" + " running it." + ) + + @cached_property + def name(self) -> str: # type: ignore + """The name of the application. This is usually the import name + with the difference that it's guessed from the run file if the + import name is main. This name is used as a display name when + Flask needs the name of the application. It can be set and overridden + to change the value. + + .. versionadded:: 0.8 + """ + if self.import_name == "__main__": + fn: str | None = getattr(sys.modules["__main__"], "__file__", None) + if fn is None: + return "__main__" + return os.path.splitext(os.path.basename(fn))[0] + return self.import_name + + @cached_property + def logger(self) -> logging.Logger: + """A standard Python :class:`~logging.Logger` for the app, with + the same name as :attr:`name`. + + In debug mode, the logger's :attr:`~logging.Logger.level` will + be set to :data:`~logging.DEBUG`. + + If there are no handlers configured, a default handler will be + added. See :doc:`/logging` for more information. + + .. versionchanged:: 1.1.0 + The logger takes the same name as :attr:`name` rather than + hard-coding ``"flask.app"``. + + .. versionchanged:: 1.0.0 + Behavior was simplified. The logger is always named + ``"flask.app"``. The level is only set during configuration, + it doesn't check ``app.debug`` each time. Only one format is + used, not different ones depending on ``app.debug``. No + handlers are removed, and a handler is only added if no + handlers are already configured. + + .. versionadded:: 0.3 + """ + return create_logger(self) + + @cached_property + def jinja_env(self) -> Environment: + """The Jinja environment used to load templates. + + The environment is created the first time this property is + accessed. Changing :attr:`jinja_options` after that will have no + effect. + """ + return self.create_jinja_environment() + + def create_jinja_environment(self) -> Environment: + raise NotImplementedError() + + def make_config(self, instance_relative: bool = False) -> Config: + """Used to create the config attribute by the Flask constructor. + The `instance_relative` parameter is passed in from the constructor + of Flask (there named `instance_relative_config`) and indicates if + the config should be relative to the instance path or the root path + of the application. + + .. versionadded:: 0.8 + """ + root_path = self.root_path + if instance_relative: + root_path = self.instance_path + defaults = dict(self.default_config) + defaults["DEBUG"] = get_debug_flag() + return self.config_class(root_path, defaults) + + def make_aborter(self) -> Aborter: + """Create the object to assign to :attr:`aborter`. That object + is called by :func:`flask.abort` to raise HTTP errors, and can + be called directly as well. + + By default, this creates an instance of :attr:`aborter_class`, + which defaults to :class:`werkzeug.exceptions.Aborter`. + + .. versionadded:: 2.2 + """ + return self.aborter_class() + + def auto_find_instance_path(self) -> str: + """Tries to locate the instance path if it was not provided to the + constructor of the application class. It will basically calculate + the path to a folder named ``instance`` next to your main file or + the package. + + .. versionadded:: 0.8 + """ + prefix, package_path = find_package(self.import_name) + if prefix is None: + return os.path.join(package_path, "instance") + return os.path.join(prefix, "var", f"{self.name}-instance") + + def create_global_jinja_loader(self) -> DispatchingJinjaLoader: + """Creates the loader for the Jinja2 environment. Can be used to + override just the loader and keeping the rest unchanged. It's + discouraged to override this function. Instead one should override + the :meth:`jinja_loader` function instead. + + The global loader dispatches between the loaders of the application + and the individual blueprints. + + .. versionadded:: 0.7 + """ + return DispatchingJinjaLoader(self) + + def select_jinja_autoescape(self, filename: str) -> bool: + """Returns ``True`` if autoescaping should be active for the given + template name. If no template name is given, returns `True`. + + .. versionchanged:: 2.2 + Autoescaping is now enabled by default for ``.svg`` files. + + .. versionadded:: 0.5 + """ + if filename is None: + return True + return filename.endswith((".html", ".htm", ".xml", ".xhtml", ".svg")) + + @property + def debug(self) -> bool: + """Whether debug mode is enabled. When using ``flask run`` to start the + development server, an interactive debugger will be shown for unhandled + exceptions, and the server will be reloaded when code changes. This maps to the + :data:`DEBUG` config key. It may not behave as expected if set late. + + **Do not enable debug mode when deploying in production.** + + Default: ``False`` + """ + return self.config["DEBUG"] # type: ignore[no-any-return] + + @debug.setter + def debug(self, value: bool) -> None: + self.config["DEBUG"] = value + + if self.config["TEMPLATES_AUTO_RELOAD"] is None: + self.jinja_env.auto_reload = value + + @setupmethod + def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on the application. Keyword + arguments passed to this method will override the defaults set on the + blueprint. + + Calls the blueprint's :meth:`~flask.Blueprint.register` method after + recording the blueprint in the application's :attr:`blueprints`. + + :param blueprint: The blueprint to register. + :param url_prefix: Blueprint routes will be prefixed with this. + :param subdomain: Blueprint routes will match on this subdomain. + :param url_defaults: Blueprint routes will use these default values for + view arguments. + :param options: Additional keyword arguments are passed to + :class:`~flask.blueprints.BlueprintSetupState`. They can be + accessed in :meth:`~flask.Blueprint.record` callbacks. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 0.7 + """ + blueprint.register(self, options) + + def iter_blueprints(self) -> t.ValuesView[Blueprint]: + """Iterates over all blueprints by the order they were registered. + + .. versionadded:: 0.11 + """ + return self.blueprints.values() + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) # type: ignore + options["endpoint"] = endpoint + methods = options.pop("methods", None) + + # if the methods are not given and the view_func object knows its + # methods we can use that instead. If neither exists, we go with + # a tuple of only ``GET`` as default. + if methods is None: + methods = getattr(view_func, "methods", None) or ("GET",) + if isinstance(methods, str): + raise TypeError( + "Allowed methods must be a list of strings, for" + ' example: @app.route(..., methods=["POST"])' + ) + methods = {item.upper() for item in methods} + + # Methods that should always be added + required_methods = set(getattr(view_func, "required_methods", ())) + + # starting with Flask 0.8 the view_func object can disable and + # force-enable the automatic options handling. + if provide_automatic_options is None: + provide_automatic_options = getattr( + view_func, "provide_automatic_options", None + ) + + if provide_automatic_options is None: + if "OPTIONS" not in methods: + provide_automatic_options = True + required_methods.add("OPTIONS") + else: + provide_automatic_options = False + + # Add the required methods now. + methods |= required_methods + + rule_obj = self.url_rule_class(rule, methods=methods, **options) + rule_obj.provide_automatic_options = provide_automatic_options # type: ignore[attr-defined] + + self.url_map.add(rule_obj) + if view_func is not None: + old_func = self.view_functions.get(endpoint) + if old_func is not None and old_func != view_func: + raise AssertionError( + "View function mapping is overwriting an existing" + f" endpoint function: {endpoint}" + ) + self.view_functions[endpoint] = view_func + + @setupmethod + def template_filter( + self, name: str | None = None + ) -> t.Callable[[T_template_filter], T_template_filter]: + """A decorator that is used to register custom template filter. + You can specify a name for the filter, otherwise the function + name will be used. Example:: + + @app.template_filter() + def reverse(s): + return s[::-1] + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def decorator(f: T_template_filter) -> T_template_filter: + self.add_template_filter(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_filter( + self, f: ft.TemplateFilterCallable, name: str | None = None + ) -> None: + """Register a custom template filter. Works exactly like the + :meth:`template_filter` decorator. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + self.jinja_env.filters[name or f.__name__] = f + + @setupmethod + def template_test( + self, name: str | None = None + ) -> t.Callable[[T_template_test], T_template_test]: + """A decorator that is used to register custom template test. + You can specify a name for the test, otherwise the function + name will be used. Example:: + + @app.template_test() + def is_prime(n): + if n == 2: + return True + for i in range(2, int(math.ceil(math.sqrt(n))) + 1): + if n % i == 0: + return False + return True + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def decorator(f: T_template_test) -> T_template_test: + self.add_template_test(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_test( + self, f: ft.TemplateTestCallable, name: str | None = None + ) -> None: + """Register a custom template test. Works exactly like the + :meth:`template_test` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + self.jinja_env.tests[name or f.__name__] = f + + @setupmethod + def template_global( + self, name: str | None = None + ) -> t.Callable[[T_template_global], T_template_global]: + """A decorator that is used to register a custom template global function. + You can specify a name for the global function, otherwise the function + name will be used. Example:: + + @app.template_global() + def double(n): + return 2 * n + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + + def decorator(f: T_template_global) -> T_template_global: + self.add_template_global(f, name=name) + return f + + return decorator + + @setupmethod + def add_template_global( + self, f: ft.TemplateGlobalCallable, name: str | None = None + ) -> None: + """Register a custom template global function. Works exactly like the + :meth:`template_global` decorator. + + .. versionadded:: 0.10 + + :param name: the optional name of the global function, otherwise the + function name will be used. + """ + self.jinja_env.globals[name or f.__name__] = f + + @setupmethod + def teardown_appcontext(self, f: T_teardown) -> T_teardown: + """Registers a function to be called when the application + context is popped. The application context is typically popped + after the request context for each request, at the end of CLI + commands, or after a manually pushed context ends. + + .. code-block:: python + + with app.app_context(): + ... + + When the ``with`` block exits (or ``ctx.pop()`` is called), the + teardown functions are called just before the app context is + made inactive. Since a request context typically also manages an + application context it would also be called when you pop a + request context. + + When a teardown function was called because of an unhandled + exception it will be passed an error object. If an + :meth:`errorhandler` is registered, it will handle the exception + and the teardown will not receive it. + + Teardown functions must avoid raising exceptions. If they + execute code that might fail they must surround that code with a + ``try``/``except`` block and log any errors. + + The return values of teardown functions are ignored. + + .. versionadded:: 0.9 + """ + self.teardown_appcontext_funcs.append(f) + return f + + @setupmethod + def shell_context_processor( + self, f: T_shell_context_processor + ) -> T_shell_context_processor: + """Registers a shell context processor function. + + .. versionadded:: 0.11 + """ + self.shell_context_processors.append(f) + return f + + def _find_error_handler( + self, e: Exception, blueprints: list[str] + ) -> ft.ErrorHandlerCallable | None: + """Return a registered error handler for an exception in this order: + blueprint handler for a specific code, app handler for a specific code, + blueprint handler for an exception class, app handler for an exception + class, or ``None`` if a suitable handler is not found. + """ + exc_class, code = self._get_exc_class_and_code(type(e)) + names = (*blueprints, None) + + for c in (code, None) if code is not None else (None,): + for name in names: + handler_map = self.error_handler_spec[name][c] + + if not handler_map: + continue + + for cls in exc_class.__mro__: + handler = handler_map.get(cls) + + if handler is not None: + return handler + return None + + def trap_http_exception(self, e: Exception) -> bool: + """Checks if an HTTP exception should be trapped or not. By default + this will return ``False`` for all exceptions except for a bad request + key error if ``TRAP_BAD_REQUEST_ERRORS`` is set to ``True``. It + also returns ``True`` if ``TRAP_HTTP_EXCEPTIONS`` is set to ``True``. + + This is called for all HTTP exceptions raised by a view function. + If it returns ``True`` for any exception the error handler for this + exception is not called and it shows up as regular exception in the + traceback. This is helpful for debugging implicitly raised HTTP + exceptions. + + .. versionchanged:: 1.0 + Bad request errors are not trapped by default in debug mode. + + .. versionadded:: 0.8 + """ + if self.config["TRAP_HTTP_EXCEPTIONS"]: + return True + + trap_bad_request = self.config["TRAP_BAD_REQUEST_ERRORS"] + + # if unset, trap key errors in debug mode + if ( + trap_bad_request is None + and self.debug + and isinstance(e, BadRequestKeyError) + ): + return True + + if trap_bad_request: + return isinstance(e, BadRequest) + + return False + + def should_ignore_error(self, error: BaseException | None) -> bool: + """This is called to figure out if an error should be ignored + or not as far as the teardown system is concerned. If this + function returns ``True`` then the teardown handlers will not be + passed the error. + + .. versionadded:: 0.10 + """ + return False + + def redirect(self, location: str, code: int = 302) -> BaseResponse: + """Create a redirect response object. + + This is called by :func:`flask.redirect`, and can be called + directly as well. + + :param location: The URL to redirect to. + :param code: The status code for the redirect. + + .. versionadded:: 2.2 + Moved from ``flask.redirect``, which calls this method. + """ + return _wz_redirect( + location, + code=code, + Response=self.response_class, # type: ignore[arg-type] + ) + + def inject_url_defaults(self, endpoint: str, values: dict[str, t.Any]) -> None: + """Injects the URL defaults for the given endpoint directly into + the values dictionary passed. This is used internally and + automatically called on URL building. + + .. versionadded:: 0.7 + """ + names: t.Iterable[str | None] = (None,) + + # url_for may be called outside a request context, parse the + # passed endpoint instead of using request.blueprints. + if "." in endpoint: + names = chain( + names, reversed(_split_blueprint_path(endpoint.rpartition(".")[0])) + ) + + for name in names: + if name in self.url_default_functions: + for func in self.url_default_functions[name]: + func(endpoint, values) + + def handle_url_build_error( + self, error: BuildError, endpoint: str, values: dict[str, t.Any] + ) -> str: + """Called by :meth:`.url_for` if a + :exc:`~werkzeug.routing.BuildError` was raised. If this returns + a value, it will be returned by ``url_for``, otherwise the error + will be re-raised. + + Each function in :attr:`url_build_error_handlers` is called with + ``error``, ``endpoint`` and ``values``. If a function returns + ``None`` or raises a ``BuildError``, it is skipped. Otherwise, + its return value is returned by ``url_for``. + + :param error: The active ``BuildError`` being handled. + :param endpoint: The endpoint being built. + :param values: The keyword arguments passed to ``url_for``. + """ + for handler in self.url_build_error_handlers: + try: + rv = handler(error, endpoint, values) + except BuildError as e: + # make error available outside except block + error = e + else: + if rv is not None: + return rv + + # Re-raise if called with an active exception, otherwise raise + # the passed in exception. + if error is sys.exc_info()[1]: + raise + + raise error diff --git a/netdeploy/lib/python3.11/site-packages/flask/sansio/blueprints.py b/netdeploy/lib/python3.11/site-packages/flask/sansio/blueprints.py new file mode 100644 index 0000000..4f912cc --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/sansio/blueprints.py @@ -0,0 +1,632 @@ +from __future__ import annotations + +import os +import typing as t +from collections import defaultdict +from functools import update_wrapper + +from .. import typing as ft +from .scaffold import _endpoint_from_view_func +from .scaffold import _sentinel +from .scaffold import Scaffold +from .scaffold import setupmethod + +if t.TYPE_CHECKING: # pragma: no cover + from .app import App + +DeferredSetupFunction = t.Callable[["BlueprintSetupState"], None] +T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) +T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) +T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_context_processor = t.TypeVar( + "T_template_context_processor", bound=ft.TemplateContextProcessorCallable +) +T_template_filter = t.TypeVar("T_template_filter", bound=ft.TemplateFilterCallable) +T_template_global = t.TypeVar("T_template_global", bound=ft.TemplateGlobalCallable) +T_template_test = t.TypeVar("T_template_test", bound=ft.TemplateTestCallable) +T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) +T_url_value_preprocessor = t.TypeVar( + "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable +) + + +class BlueprintSetupState: + """Temporary holder object for registering a blueprint with the + application. An instance of this class is created by the + :meth:`~flask.Blueprint.make_setup_state` method and later passed + to all register callback functions. + """ + + def __init__( + self, + blueprint: Blueprint, + app: App, + options: t.Any, + first_registration: bool, + ) -> None: + #: a reference to the current application + self.app = app + + #: a reference to the blueprint that created this setup state. + self.blueprint = blueprint + + #: a dictionary with all options that were passed to the + #: :meth:`~flask.Flask.register_blueprint` method. + self.options = options + + #: as blueprints can be registered multiple times with the + #: application and not everything wants to be registered + #: multiple times on it, this attribute can be used to figure + #: out if the blueprint was registered in the past already. + self.first_registration = first_registration + + subdomain = self.options.get("subdomain") + if subdomain is None: + subdomain = self.blueprint.subdomain + + #: The subdomain that the blueprint should be active for, ``None`` + #: otherwise. + self.subdomain = subdomain + + url_prefix = self.options.get("url_prefix") + if url_prefix is None: + url_prefix = self.blueprint.url_prefix + #: The prefix that should be used for all URLs defined on the + #: blueprint. + self.url_prefix = url_prefix + + self.name = self.options.get("name", blueprint.name) + self.name_prefix = self.options.get("name_prefix", "") + + #: A dictionary with URL defaults that is added to each and every + #: URL that was defined with the blueprint. + self.url_defaults = dict(self.blueprint.url_values_defaults) + self.url_defaults.update(self.options.get("url_defaults", ())) + + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + **options: t.Any, + ) -> None: + """A helper method to register a rule (and optionally a view function) + to the application. The endpoint is automatically prefixed with the + blueprint's name. + """ + if self.url_prefix is not None: + if rule: + rule = "/".join((self.url_prefix.rstrip("/"), rule.lstrip("/"))) + else: + rule = self.url_prefix + options.setdefault("subdomain", self.subdomain) + if endpoint is None: + endpoint = _endpoint_from_view_func(view_func) # type: ignore + defaults = self.url_defaults + if "defaults" in options: + defaults = dict(defaults, **options.pop("defaults")) + + self.app.add_url_rule( + rule, + f"{self.name_prefix}.{self.name}.{endpoint}".lstrip("."), + view_func, + defaults=defaults, + **options, + ) + + +class Blueprint(Scaffold): + """Represents a blueprint, a collection of routes and other + app-related functions that can be registered on a real application + later. + + A blueprint is an object that allows defining application functions + without requiring an application object ahead of time. It uses the + same decorators as :class:`~flask.Flask`, but defers the need for an + application by recording them for later registration. + + Decorating a function with a blueprint creates a deferred function + that is called with :class:`~flask.blueprints.BlueprintSetupState` + when the blueprint is registered on an application. + + See :doc:`/blueprints` for more information. + + :param name: The name of the blueprint. Will be prepended to each + endpoint name. + :param import_name: The name of the blueprint package, usually + ``__name__``. This helps locate the ``root_path`` for the + blueprint. + :param static_folder: A folder with static files that should be + served by the blueprint's static route. The path is relative to + the blueprint's root path. Blueprint static files are disabled + by default. + :param static_url_path: The url to serve static files from. + Defaults to ``static_folder``. If the blueprint does not have + a ``url_prefix``, the app's static route will take precedence, + and the blueprint's static files won't be accessible. + :param template_folder: A folder with templates that should be added + to the app's template search path. The path is relative to the + blueprint's root path. Blueprint templates are disabled by + default. Blueprint templates have a lower precedence than those + in the app's templates folder. + :param url_prefix: A path to prepend to all of the blueprint's URLs, + to make them distinct from the rest of the app's routes. + :param subdomain: A subdomain that blueprint routes will match on by + default. + :param url_defaults: A dict of default values that blueprint routes + will receive by default. + :param root_path: By default, the blueprint will automatically set + this based on ``import_name``. In certain situations this + automatic detection can fail, so the path can be specified + manually instead. + + .. versionchanged:: 1.1.0 + Blueprints have a ``cli`` group to register nested CLI commands. + The ``cli_group`` parameter controls the name of the group under + the ``flask`` command. + + .. versionadded:: 0.7 + """ + + _got_registered_once = False + + def __init__( + self, + name: str, + import_name: str, + static_folder: str | os.PathLike[str] | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike[str] | None = None, + url_prefix: str | None = None, + subdomain: str | None = None, + url_defaults: dict[str, t.Any] | None = None, + root_path: str | None = None, + cli_group: str | None = _sentinel, # type: ignore[assignment] + ): + super().__init__( + import_name=import_name, + static_folder=static_folder, + static_url_path=static_url_path, + template_folder=template_folder, + root_path=root_path, + ) + + if not name: + raise ValueError("'name' may not be empty.") + + if "." in name: + raise ValueError("'name' may not contain a dot '.' character.") + + self.name = name + self.url_prefix = url_prefix + self.subdomain = subdomain + self.deferred_functions: list[DeferredSetupFunction] = [] + + if url_defaults is None: + url_defaults = {} + + self.url_values_defaults = url_defaults + self.cli_group = cli_group + self._blueprints: list[tuple[Blueprint, dict[str, t.Any]]] = [] + + def _check_setup_finished(self, f_name: str) -> None: + if self._got_registered_once: + raise AssertionError( + f"The setup method '{f_name}' can no longer be called on the blueprint" + f" '{self.name}'. It has already been registered at least once, any" + " changes will not be applied consistently.\n" + "Make sure all imports, decorators, functions, etc. needed to set up" + " the blueprint are done before registering it." + ) + + @setupmethod + def record(self, func: DeferredSetupFunction) -> None: + """Registers a function that is called when the blueprint is + registered on the application. This function is called with the + state as argument as returned by the :meth:`make_setup_state` + method. + """ + self.deferred_functions.append(func) + + @setupmethod + def record_once(self, func: DeferredSetupFunction) -> None: + """Works like :meth:`record` but wraps the function in another + function that will ensure the function is only called once. If the + blueprint is registered a second time on the application, the + function passed is not called. + """ + + def wrapper(state: BlueprintSetupState) -> None: + if state.first_registration: + func(state) + + self.record(update_wrapper(wrapper, func)) + + def make_setup_state( + self, app: App, options: dict[str, t.Any], first_registration: bool = False + ) -> BlueprintSetupState: + """Creates an instance of :meth:`~flask.blueprints.BlueprintSetupState` + object that is later passed to the register callback functions. + Subclasses can override this to return a subclass of the setup state. + """ + return BlueprintSetupState(self, app, options, first_registration) + + @setupmethod + def register_blueprint(self, blueprint: Blueprint, **options: t.Any) -> None: + """Register a :class:`~flask.Blueprint` on this blueprint. Keyword + arguments passed to this method will override the defaults set + on the blueprint. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + + .. versionadded:: 2.0 + """ + if blueprint is self: + raise ValueError("Cannot register a blueprint on itself") + self._blueprints.append((blueprint, options)) + + def register(self, app: App, options: dict[str, t.Any]) -> None: + """Called by :meth:`Flask.register_blueprint` to register all + views and callbacks registered on the blueprint with the + application. Creates a :class:`.BlueprintSetupState` and calls + each :meth:`record` callback with it. + + :param app: The application this blueprint is being registered + with. + :param options: Keyword arguments forwarded from + :meth:`~Flask.register_blueprint`. + + .. versionchanged:: 2.3 + Nested blueprints now correctly apply subdomains. + + .. versionchanged:: 2.1 + Registering the same blueprint with the same name multiple + times is an error. + + .. versionchanged:: 2.0.1 + Nested blueprints are registered with their dotted name. + This allows different blueprints with the same name to be + nested at different locations. + + .. versionchanged:: 2.0.1 + The ``name`` option can be used to change the (pre-dotted) + name the blueprint is registered with. This allows the same + blueprint to be registered multiple times with unique names + for ``url_for``. + """ + name_prefix = options.get("name_prefix", "") + self_name = options.get("name", self.name) + name = f"{name_prefix}.{self_name}".lstrip(".") + + if name in app.blueprints: + bp_desc = "this" if app.blueprints[name] is self else "a different" + existing_at = f" '{name}'" if self_name != name else "" + + raise ValueError( + f"The name '{self_name}' is already registered for" + f" {bp_desc} blueprint{existing_at}. Use 'name=' to" + f" provide a unique name." + ) + + first_bp_registration = not any(bp is self for bp in app.blueprints.values()) + first_name_registration = name not in app.blueprints + + app.blueprints[name] = self + self._got_registered_once = True + state = self.make_setup_state(app, options, first_bp_registration) + + if self.has_static_folder: + state.add_url_rule( + f"{self.static_url_path}/", + view_func=self.send_static_file, # type: ignore[attr-defined] + endpoint="static", + ) + + # Merge blueprint data into parent. + if first_bp_registration or first_name_registration: + self._merge_blueprint_funcs(app, name) + + for deferred in self.deferred_functions: + deferred(state) + + cli_resolved_group = options.get("cli_group", self.cli_group) + + if self.cli.commands: + if cli_resolved_group is None: + app.cli.commands.update(self.cli.commands) + elif cli_resolved_group is _sentinel: + self.cli.name = name + app.cli.add_command(self.cli) + else: + self.cli.name = cli_resolved_group + app.cli.add_command(self.cli) + + for blueprint, bp_options in self._blueprints: + bp_options = bp_options.copy() + bp_url_prefix = bp_options.get("url_prefix") + bp_subdomain = bp_options.get("subdomain") + + if bp_subdomain is None: + bp_subdomain = blueprint.subdomain + + if state.subdomain is not None and bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + "." + state.subdomain + elif bp_subdomain is not None: + bp_options["subdomain"] = bp_subdomain + elif state.subdomain is not None: + bp_options["subdomain"] = state.subdomain + + if bp_url_prefix is None: + bp_url_prefix = blueprint.url_prefix + + if state.url_prefix is not None and bp_url_prefix is not None: + bp_options["url_prefix"] = ( + state.url_prefix.rstrip("/") + "/" + bp_url_prefix.lstrip("/") + ) + elif bp_url_prefix is not None: + bp_options["url_prefix"] = bp_url_prefix + elif state.url_prefix is not None: + bp_options["url_prefix"] = state.url_prefix + + bp_options["name_prefix"] = name + blueprint.register(app, bp_options) + + def _merge_blueprint_funcs(self, app: App, name: str) -> None: + def extend( + bp_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], + parent_dict: dict[ft.AppOrBlueprintKey, list[t.Any]], + ) -> None: + for key, values in bp_dict.items(): + key = name if key is None else f"{name}.{key}" + parent_dict[key].extend(values) + + for key, value in self.error_handler_spec.items(): + key = name if key is None else f"{name}.{key}" + value = defaultdict( + dict, + { + code: {exc_class: func for exc_class, func in code_values.items()} + for code, code_values in value.items() + }, + ) + app.error_handler_spec[key] = value + + for endpoint, func in self.view_functions.items(): + app.view_functions[endpoint] = func + + extend(self.before_request_funcs, app.before_request_funcs) + extend(self.after_request_funcs, app.after_request_funcs) + extend( + self.teardown_request_funcs, + app.teardown_request_funcs, + ) + extend(self.url_default_functions, app.url_default_functions) + extend(self.url_value_preprocessors, app.url_value_preprocessors) + extend(self.template_context_processors, app.template_context_processors) + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + """Register a URL rule with the blueprint. See :meth:`.Flask.add_url_rule` for + full documentation. + + The URL rule is prefixed with the blueprint's URL prefix. The endpoint name, + used with :func:`url_for`, is prefixed with the blueprint's name. + """ + if endpoint and "." in endpoint: + raise ValueError("'endpoint' may not contain a dot '.' character.") + + if view_func and hasattr(view_func, "__name__") and "." in view_func.__name__: + raise ValueError("'view_func' name may not contain a dot '.' character.") + + self.record( + lambda s: s.add_url_rule( + rule, + endpoint, + view_func, + provide_automatic_options=provide_automatic_options, + **options, + ) + ) + + @setupmethod + def app_template_filter( + self, name: str | None = None + ) -> t.Callable[[T_template_filter], T_template_filter]: + """Register a template filter, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_filter`. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def decorator(f: T_template_filter) -> T_template_filter: + self.add_app_template_filter(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_filter( + self, f: ft.TemplateFilterCallable, name: str | None = None + ) -> None: + """Register a template filter, available in any template rendered by the + application. Works like the :meth:`app_template_filter` decorator. Equivalent to + :meth:`.Flask.add_template_filter`. + + :param name: the optional name of the filter, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.filters[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def app_template_test( + self, name: str | None = None + ) -> t.Callable[[T_template_test], T_template_test]: + """Register a template test, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def decorator(f: T_template_test) -> T_template_test: + self.add_app_template_test(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_test( + self, f: ft.TemplateTestCallable, name: str | None = None + ) -> None: + """Register a template test, available in any template rendered by the + application. Works like the :meth:`app_template_test` decorator. Equivalent to + :meth:`.Flask.add_template_test`. + + .. versionadded:: 0.10 + + :param name: the optional name of the test, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.tests[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def app_template_global( + self, name: str | None = None + ) -> t.Callable[[T_template_global], T_template_global]: + """Register a template global, available in any template rendered by the + application. Equivalent to :meth:`.Flask.template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def decorator(f: T_template_global) -> T_template_global: + self.add_app_template_global(f, name=name) + return f + + return decorator + + @setupmethod + def add_app_template_global( + self, f: ft.TemplateGlobalCallable, name: str | None = None + ) -> None: + """Register a template global, available in any template rendered by the + application. Works like the :meth:`app_template_global` decorator. Equivalent to + :meth:`.Flask.add_template_global`. + + .. versionadded:: 0.10 + + :param name: the optional name of the global, otherwise the + function name will be used. + """ + + def register_template(state: BlueprintSetupState) -> None: + state.app.jinja_env.globals[name or f.__name__] = f + + self.record_once(register_template) + + @setupmethod + def before_app_request(self, f: T_before_request) -> T_before_request: + """Like :meth:`before_request`, but before every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.before_request`. + """ + self.record_once( + lambda s: s.app.before_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def after_app_request(self, f: T_after_request) -> T_after_request: + """Like :meth:`after_request`, but after every request, not only those handled + by the blueprint. Equivalent to :meth:`.Flask.after_request`. + """ + self.record_once( + lambda s: s.app.after_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def teardown_app_request(self, f: T_teardown) -> T_teardown: + """Like :meth:`teardown_request`, but after every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.teardown_request`. + """ + self.record_once( + lambda s: s.app.teardown_request_funcs.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_context_processor( + self, f: T_template_context_processor + ) -> T_template_context_processor: + """Like :meth:`context_processor`, but for templates rendered by every view, not + only by the blueprint. Equivalent to :meth:`.Flask.context_processor`. + """ + self.record_once( + lambda s: s.app.template_context_processors.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_errorhandler( + self, code: type[Exception] | int + ) -> t.Callable[[T_error_handler], T_error_handler]: + """Like :meth:`errorhandler`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.errorhandler`. + """ + + def decorator(f: T_error_handler) -> T_error_handler: + def from_blueprint(state: BlueprintSetupState) -> None: + state.app.errorhandler(code)(f) + + self.record_once(from_blueprint) + return f + + return decorator + + @setupmethod + def app_url_value_preprocessor( + self, f: T_url_value_preprocessor + ) -> T_url_value_preprocessor: + """Like :meth:`url_value_preprocessor`, but for every request, not only those + handled by the blueprint. Equivalent to :meth:`.Flask.url_value_preprocessor`. + """ + self.record_once( + lambda s: s.app.url_value_preprocessors.setdefault(None, []).append(f) + ) + return f + + @setupmethod + def app_url_defaults(self, f: T_url_defaults) -> T_url_defaults: + """Like :meth:`url_defaults`, but for every request, not only those handled by + the blueprint. Equivalent to :meth:`.Flask.url_defaults`. + """ + self.record_once( + lambda s: s.app.url_default_functions.setdefault(None, []).append(f) + ) + return f diff --git a/netdeploy/lib/python3.11/site-packages/flask/sansio/scaffold.py b/netdeploy/lib/python3.11/site-packages/flask/sansio/scaffold.py new file mode 100644 index 0000000..69e33a0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/sansio/scaffold.py @@ -0,0 +1,801 @@ +from __future__ import annotations + +import importlib.util +import os +import pathlib +import sys +import typing as t +from collections import defaultdict +from functools import update_wrapper + +from jinja2 import BaseLoader +from jinja2 import FileSystemLoader +from werkzeug.exceptions import default_exceptions +from werkzeug.exceptions import HTTPException +from werkzeug.utils import cached_property + +from .. import typing as ft +from ..helpers import get_root_path +from ..templating import _default_template_ctx_processor + +if t.TYPE_CHECKING: # pragma: no cover + from click import Group + +# a singleton sentinel value for parameter defaults +_sentinel = object() + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) +T_after_request = t.TypeVar("T_after_request", bound=ft.AfterRequestCallable[t.Any]) +T_before_request = t.TypeVar("T_before_request", bound=ft.BeforeRequestCallable) +T_error_handler = t.TypeVar("T_error_handler", bound=ft.ErrorHandlerCallable) +T_teardown = t.TypeVar("T_teardown", bound=ft.TeardownCallable) +T_template_context_processor = t.TypeVar( + "T_template_context_processor", bound=ft.TemplateContextProcessorCallable +) +T_url_defaults = t.TypeVar("T_url_defaults", bound=ft.URLDefaultCallable) +T_url_value_preprocessor = t.TypeVar( + "T_url_value_preprocessor", bound=ft.URLValuePreprocessorCallable +) +T_route = t.TypeVar("T_route", bound=ft.RouteCallable) + + +def setupmethod(f: F) -> F: + f_name = f.__name__ + + def wrapper_func(self: Scaffold, *args: t.Any, **kwargs: t.Any) -> t.Any: + self._check_setup_finished(f_name) + return f(self, *args, **kwargs) + + return t.cast(F, update_wrapper(wrapper_func, f)) + + +class Scaffold: + """Common behavior shared between :class:`~flask.Flask` and + :class:`~flask.blueprints.Blueprint`. + + :param import_name: The import name of the module where this object + is defined. Usually :attr:`__name__` should be used. + :param static_folder: Path to a folder of static files to serve. + If this is set, a static route will be added. + :param static_url_path: URL prefix for the static route. + :param template_folder: Path to a folder containing template files. + for rendering. If this is set, a Jinja loader will be added. + :param root_path: The path that static, template, and resource files + are relative to. Typically not set, it is discovered based on + the ``import_name``. + + .. versionadded:: 2.0 + """ + + cli: Group + name: str + _static_folder: str | None = None + _static_url_path: str | None = None + + def __init__( + self, + import_name: str, + static_folder: str | os.PathLike[str] | None = None, + static_url_path: str | None = None, + template_folder: str | os.PathLike[str] | None = None, + root_path: str | None = None, + ): + #: The name of the package or module that this object belongs + #: to. Do not change this once it is set by the constructor. + self.import_name = import_name + + self.static_folder = static_folder # type: ignore + self.static_url_path = static_url_path + + #: The path to the templates folder, relative to + #: :attr:`root_path`, to add to the template loader. ``None`` if + #: templates should not be added. + self.template_folder = template_folder + + if root_path is None: + root_path = get_root_path(self.import_name) + + #: Absolute path to the package on the filesystem. Used to look + #: up resources contained in the package. + self.root_path = root_path + + #: A dictionary mapping endpoint names to view functions. + #: + #: To register a view function, use the :meth:`route` decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.view_functions: dict[str, ft.RouteCallable] = {} + + #: A data structure of registered error handlers, in the format + #: ``{scope: {code: {class: handler}}}``. The ``scope`` key is + #: the name of a blueprint the handlers are active for, or + #: ``None`` for all requests. The ``code`` key is the HTTP + #: status code for ``HTTPException``, or ``None`` for + #: other exceptions. The innermost dictionary maps exception + #: classes to handler functions. + #: + #: To register an error handler, use the :meth:`errorhandler` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.error_handler_spec: dict[ + ft.AppOrBlueprintKey, + dict[int | None, dict[type[Exception], ft.ErrorHandlerCallable]], + ] = defaultdict(lambda: defaultdict(dict)) + + #: A data structure of functions to call at the beginning of + #: each request, in the format ``{scope: [functions]}``. The + #: ``scope`` key is the name of a blueprint the functions are + #: active for, or ``None`` for all requests. + #: + #: To register a function, use the :meth:`before_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.before_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.BeforeRequestCallable] + ] = defaultdict(list) + + #: A data structure of functions to call at the end of each + #: request, in the format ``{scope: [functions]}``. The + #: ``scope`` key is the name of a blueprint the functions are + #: active for, or ``None`` for all requests. + #: + #: To register a function, use the :meth:`after_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.after_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.AfterRequestCallable[t.Any]] + ] = defaultdict(list) + + #: A data structure of functions to call at the end of each + #: request even if an exception is raised, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`teardown_request` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.teardown_request_funcs: dict[ + ft.AppOrBlueprintKey, list[ft.TeardownCallable] + ] = defaultdict(list) + + #: A data structure of functions to call to pass extra context + #: values when rendering templates, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`context_processor` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.template_context_processors: dict[ + ft.AppOrBlueprintKey, list[ft.TemplateContextProcessorCallable] + ] = defaultdict(list, {None: [_default_template_ctx_processor]}) + + #: A data structure of functions to call to modify the keyword + #: arguments passed to the view function, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the + #: :meth:`url_value_preprocessor` decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.url_value_preprocessors: dict[ + ft.AppOrBlueprintKey, + list[ft.URLValuePreprocessorCallable], + ] = defaultdict(list) + + #: A data structure of functions to call to modify the keyword + #: arguments when generating URLs, in the format + #: ``{scope: [functions]}``. The ``scope`` key is the name of a + #: blueprint the functions are active for, or ``None`` for all + #: requests. + #: + #: To register a function, use the :meth:`url_defaults` + #: decorator. + #: + #: This data structure is internal. It should not be modified + #: directly and its format may change at any time. + self.url_default_functions: dict[ + ft.AppOrBlueprintKey, list[ft.URLDefaultCallable] + ] = defaultdict(list) + + def __repr__(self) -> str: + return f"<{type(self).__name__} {self.name!r}>" + + def _check_setup_finished(self, f_name: str) -> None: + raise NotImplementedError + + @property + def static_folder(self) -> str | None: + """The absolute path to the configured static folder. ``None`` + if no static folder is set. + """ + if self._static_folder is not None: + return os.path.join(self.root_path, self._static_folder) + else: + return None + + @static_folder.setter + def static_folder(self, value: str | os.PathLike[str] | None) -> None: + if value is not None: + value = os.fspath(value).rstrip(r"\/") + + self._static_folder = value + + @property + def has_static_folder(self) -> bool: + """``True`` if :attr:`static_folder` is set. + + .. versionadded:: 0.5 + """ + return self.static_folder is not None + + @property + def static_url_path(self) -> str | None: + """The URL prefix that the static route will be accessible from. + + If it was not configured during init, it is derived from + :attr:`static_folder`. + """ + if self._static_url_path is not None: + return self._static_url_path + + if self.static_folder is not None: + basename = os.path.basename(self.static_folder) + return f"/{basename}".rstrip("/") + + return None + + @static_url_path.setter + def static_url_path(self, value: str | None) -> None: + if value is not None: + value = value.rstrip("/") + + self._static_url_path = value + + @cached_property + def jinja_loader(self) -> BaseLoader | None: + """The Jinja loader for this object's templates. By default this + is a class :class:`jinja2.loaders.FileSystemLoader` to + :attr:`template_folder` if it is set. + + .. versionadded:: 0.5 + """ + if self.template_folder is not None: + return FileSystemLoader(os.path.join(self.root_path, self.template_folder)) + else: + return None + + def _method_route( + self, + method: str, + rule: str, + options: dict[str, t.Any], + ) -> t.Callable[[T_route], T_route]: + if "methods" in options: + raise TypeError("Use the 'route' decorator to use the 'methods' argument.") + + return self.route(rule, methods=[method], **options) + + @setupmethod + def get(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["GET"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("GET", rule, options) + + @setupmethod + def post(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["POST"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("POST", rule, options) + + @setupmethod + def put(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["PUT"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("PUT", rule, options) + + @setupmethod + def delete(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["DELETE"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("DELETE", rule, options) + + @setupmethod + def patch(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Shortcut for :meth:`route` with ``methods=["PATCH"]``. + + .. versionadded:: 2.0 + """ + return self._method_route("PATCH", rule, options) + + @setupmethod + def route(self, rule: str, **options: t.Any) -> t.Callable[[T_route], T_route]: + """Decorate a view function to register it with the given URL + rule and options. Calls :meth:`add_url_rule`, which has more + details about the implementation. + + .. code-block:: python + + @app.route("/") + def index(): + return "Hello, World!" + + See :ref:`url-route-registrations`. + + The endpoint name for the route defaults to the name of the view + function if the ``endpoint`` parameter isn't passed. + + The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` and + ``OPTIONS`` are added automatically. + + :param rule: The URL rule string. + :param options: Extra options passed to the + :class:`~werkzeug.routing.Rule` object. + """ + + def decorator(f: T_route) -> T_route: + endpoint = options.pop("endpoint", None) + self.add_url_rule(rule, endpoint, f, **options) + return f + + return decorator + + @setupmethod + def add_url_rule( + self, + rule: str, + endpoint: str | None = None, + view_func: ft.RouteCallable | None = None, + provide_automatic_options: bool | None = None, + **options: t.Any, + ) -> None: + """Register a rule for routing incoming requests and building + URLs. The :meth:`route` decorator is a shortcut to call this + with the ``view_func`` argument. These are equivalent: + + .. code-block:: python + + @app.route("/") + def index(): + ... + + .. code-block:: python + + def index(): + ... + + app.add_url_rule("/", view_func=index) + + See :ref:`url-route-registrations`. + + The endpoint name for the route defaults to the name of the view + function if the ``endpoint`` parameter isn't passed. An error + will be raised if a function has already been registered for the + endpoint. + + The ``methods`` parameter defaults to ``["GET"]``. ``HEAD`` is + always added automatically, and ``OPTIONS`` is added + automatically by default. + + ``view_func`` does not necessarily need to be passed, but if the + rule should participate in routing an endpoint name must be + associated with a view function at some point with the + :meth:`endpoint` decorator. + + .. code-block:: python + + app.add_url_rule("/", endpoint="index") + + @app.endpoint("index") + def index(): + ... + + If ``view_func`` has a ``required_methods`` attribute, those + methods are added to the passed and automatic methods. If it + has a ``provide_automatic_methods`` attribute, it is used as the + default if the parameter is not passed. + + :param rule: The URL rule string. + :param endpoint: The endpoint name to associate with the rule + and view function. Used when routing and building URLs. + Defaults to ``view_func.__name__``. + :param view_func: The view function to associate with the + endpoint name. + :param provide_automatic_options: Add the ``OPTIONS`` method and + respond to ``OPTIONS`` requests automatically. + :param options: Extra options passed to the + :class:`~werkzeug.routing.Rule` object. + """ + raise NotImplementedError + + @setupmethod + def endpoint(self, endpoint: str) -> t.Callable[[F], F]: + """Decorate a view function to register it for the given + endpoint. Used if a rule is added without a ``view_func`` with + :meth:`add_url_rule`. + + .. code-block:: python + + app.add_url_rule("/ex", endpoint="example") + + @app.endpoint("example") + def example(): + ... + + :param endpoint: The endpoint name to associate with the view + function. + """ + + def decorator(f: F) -> F: + self.view_functions[endpoint] = f + return f + + return decorator + + @setupmethod + def before_request(self, f: T_before_request) -> T_before_request: + """Register a function to run before each request. + + For example, this can be used to open a database connection, or + to load the logged in user from the session. + + .. code-block:: python + + @app.before_request + def load_user(): + if "user_id" in session: + g.user = db.session.get(session["user_id"]) + + The function will be called without any arguments. If it returns + a non-``None`` value, the value is handled as if it was the + return value from the view, and further request handling is + stopped. + + This is available on both app and blueprint objects. When used on an app, this + executes before every request. When used on a blueprint, this executes before + every request that the blueprint handles. To register with a blueprint and + execute before every request, use :meth:`.Blueprint.before_app_request`. + """ + self.before_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def after_request(self, f: T_after_request) -> T_after_request: + """Register a function to run after each request to this object. + + The function is called with the response object, and must return + a response object. This allows the functions to modify or + replace the response before it is sent. + + If a function raises an exception, any remaining + ``after_request`` functions will not be called. Therefore, this + should not be used for actions that must execute, such as to + close resources. Use :meth:`teardown_request` for that. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.after_app_request`. + """ + self.after_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def teardown_request(self, f: T_teardown) -> T_teardown: + """Register a function to be called when the request context is + popped. Typically this happens at the end of each request, but + contexts may be pushed manually as well during testing. + + .. code-block:: python + + with app.test_request_context(): + ... + + When the ``with`` block exits (or ``ctx.pop()`` is called), the + teardown functions are called just before the request context is + made inactive. + + When a teardown function was called because of an unhandled + exception it will be passed an error object. If an + :meth:`errorhandler` is registered, it will handle the exception + and the teardown will not receive it. + + Teardown functions must avoid raising exceptions. If they + execute code that might fail they must surround that code with a + ``try``/``except`` block and log any errors. + + The return values of teardown functions are ignored. + + This is available on both app and blueprint objects. When used on an app, this + executes after every request. When used on a blueprint, this executes after + every request that the blueprint handles. To register with a blueprint and + execute after every request, use :meth:`.Blueprint.teardown_app_request`. + """ + self.teardown_request_funcs.setdefault(None, []).append(f) + return f + + @setupmethod + def context_processor( + self, + f: T_template_context_processor, + ) -> T_template_context_processor: + """Registers a template context processor function. These functions run before + rendering a template. The keys of the returned dict are added as variables + available in the template. + + This is available on both app and blueprint objects. When used on an app, this + is called for every rendered template. When used on a blueprint, this is called + for templates rendered from the blueprint's views. To register with a blueprint + and affect every template, use :meth:`.Blueprint.app_context_processor`. + """ + self.template_context_processors[None].append(f) + return f + + @setupmethod + def url_value_preprocessor( + self, + f: T_url_value_preprocessor, + ) -> T_url_value_preprocessor: + """Register a URL value preprocessor function for all view + functions in the application. These functions will be called before the + :meth:`before_request` functions. + + The function can modify the values captured from the matched url before + they are passed to the view. For example, this can be used to pop a + common language code value and place it in ``g`` rather than pass it to + every view. + + The function is passed the endpoint name and values dict. The return + value is ignored. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_value_preprocessor`. + """ + self.url_value_preprocessors[None].append(f) + return f + + @setupmethod + def url_defaults(self, f: T_url_defaults) -> T_url_defaults: + """Callback function for URL defaults for all view functions of the + application. It's called with the endpoint and values and should + update the values passed in place. + + This is available on both app and blueprint objects. When used on an app, this + is called for every request. When used on a blueprint, this is called for + requests that the blueprint handles. To register with a blueprint and affect + every request, use :meth:`.Blueprint.app_url_defaults`. + """ + self.url_default_functions[None].append(f) + return f + + @setupmethod + def errorhandler( + self, code_or_exception: type[Exception] | int + ) -> t.Callable[[T_error_handler], T_error_handler]: + """Register a function to handle errors by code or exception class. + + A decorator that is used to register a function given an + error code. Example:: + + @app.errorhandler(404) + def page_not_found(error): + return 'This page does not exist', 404 + + You can also register handlers for arbitrary exceptions:: + + @app.errorhandler(DatabaseError) + def special_exception_handler(error): + return 'Database connection failed', 500 + + This is available on both app and blueprint objects. When used on an app, this + can handle errors from every request. When used on a blueprint, this can handle + errors from requests that the blueprint handles. To register with a blueprint + and affect every request, use :meth:`.Blueprint.app_errorhandler`. + + .. versionadded:: 0.7 + Use :meth:`register_error_handler` instead of modifying + :attr:`error_handler_spec` directly, for application wide error + handlers. + + .. versionadded:: 0.7 + One can now additionally also register custom exception types + that do not necessarily have to be a subclass of the + :class:`~werkzeug.exceptions.HTTPException` class. + + :param code_or_exception: the code as integer for the handler, or + an arbitrary exception + """ + + def decorator(f: T_error_handler) -> T_error_handler: + self.register_error_handler(code_or_exception, f) + return f + + return decorator + + @setupmethod + def register_error_handler( + self, + code_or_exception: type[Exception] | int, + f: ft.ErrorHandlerCallable, + ) -> None: + """Alternative error attach function to the :meth:`errorhandler` + decorator that is more straightforward to use for non decorator + usage. + + .. versionadded:: 0.7 + """ + exc_class, code = self._get_exc_class_and_code(code_or_exception) + self.error_handler_spec[None][code][exc_class] = f + + @staticmethod + def _get_exc_class_and_code( + exc_class_or_code: type[Exception] | int, + ) -> tuple[type[Exception], int | None]: + """Get the exception class being handled. For HTTP status codes + or ``HTTPException`` subclasses, return both the exception and + status code. + + :param exc_class_or_code: Any exception class, or an HTTP status + code as an integer. + """ + exc_class: type[Exception] + + if isinstance(exc_class_or_code, int): + try: + exc_class = default_exceptions[exc_class_or_code] + except KeyError: + raise ValueError( + f"'{exc_class_or_code}' is not a recognized HTTP" + " error code. Use a subclass of HTTPException with" + " that code instead." + ) from None + else: + exc_class = exc_class_or_code + + if isinstance(exc_class, Exception): + raise TypeError( + f"{exc_class!r} is an instance, not a class. Handlers" + " can only be registered for Exception classes or HTTP" + " error codes." + ) + + if not issubclass(exc_class, Exception): + raise ValueError( + f"'{exc_class.__name__}' is not a subclass of Exception." + " Handlers can only be registered for Exception classes" + " or HTTP error codes." + ) + + if issubclass(exc_class, HTTPException): + return exc_class, exc_class.code + else: + return exc_class, None + + +def _endpoint_from_view_func(view_func: ft.RouteCallable) -> str: + """Internal helper that returns the default endpoint for a given + function. This always is the function name. + """ + assert view_func is not None, "expected view func if endpoint is not provided." + return view_func.__name__ + + +def _path_is_relative_to(path: pathlib.PurePath, base: str) -> bool: + # Path.is_relative_to doesn't exist until Python 3.9 + try: + path.relative_to(base) + return True + except ValueError: + return False + + +def _find_package_path(import_name: str) -> str: + """Find the path that contains the package or module.""" + root_mod_name, _, _ = import_name.partition(".") + + try: + root_spec = importlib.util.find_spec(root_mod_name) + + if root_spec is None: + raise ValueError("not found") + except (ImportError, ValueError): + # ImportError: the machinery told us it does not exist + # ValueError: + # - the module name was invalid + # - the module name is __main__ + # - we raised `ValueError` due to `root_spec` being `None` + return os.getcwd() + + if root_spec.submodule_search_locations: + if root_spec.origin is None or root_spec.origin == "namespace": + # namespace package + package_spec = importlib.util.find_spec(import_name) + + if package_spec is not None and package_spec.submodule_search_locations: + # Pick the path in the namespace that contains the submodule. + package_path = pathlib.Path( + os.path.commonpath(package_spec.submodule_search_locations) + ) + search_location = next( + location + for location in root_spec.submodule_search_locations + if _path_is_relative_to(package_path, location) + ) + else: + # Pick the first path. + search_location = root_spec.submodule_search_locations[0] + + return os.path.dirname(search_location) + else: + # package with __init__.py + return os.path.dirname(os.path.dirname(root_spec.origin)) + else: + # module + return os.path.dirname(root_spec.origin) # type: ignore[type-var, return-value] + + +def find_package(import_name: str) -> tuple[str | None, str]: + """Find the prefix that a package is installed under, and the path + that it would be imported from. + + The prefix is the directory containing the standard directory + hierarchy (lib, bin, etc.). If the package is not installed to the + system (:attr:`sys.prefix`) or a virtualenv (``site-packages``), + ``None`` is returned. + + The path is the entry in :attr:`sys.path` that contains the package + for import. If the package is not installed, it's assumed that the + package was imported from the current working directory. + """ + package_path = _find_package_path(import_name) + py_prefix = os.path.abspath(sys.prefix) + + # installed to the system + if _path_is_relative_to(pathlib.PurePath(package_path), py_prefix): + return py_prefix, package_path + + site_parent, site_folder = os.path.split(package_path) + + # installed to a virtualenv + if site_folder.lower() == "site-packages": + parent, folder = os.path.split(site_parent) + + # Windows (prefix/lib/site-packages) + if folder.lower() == "lib": + return parent, package_path + + # Unix (prefix/lib/pythonX.Y/site-packages) + if os.path.basename(parent).lower() == "lib": + return os.path.dirname(parent), package_path + + # something else (prefix/site-packages) + return site_parent, package_path + + # not installed + return None, package_path diff --git a/netdeploy/lib/python3.11/site-packages/flask/sessions.py b/netdeploy/lib/python3.11/site-packages/flask/sessions.py new file mode 100644 index 0000000..ee19ad6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/sessions.py @@ -0,0 +1,379 @@ +from __future__ import annotations + +import hashlib +import typing as t +from collections.abc import MutableMapping +from datetime import datetime +from datetime import timezone + +from itsdangerous import BadSignature +from itsdangerous import URLSafeTimedSerializer +from werkzeug.datastructures import CallbackDict + +from .json.tag import TaggedJSONSerializer + +if t.TYPE_CHECKING: # pragma: no cover + import typing_extensions as te + + from .app import Flask + from .wrappers import Request + from .wrappers import Response + + +# TODO generic when Python > 3.8 +class SessionMixin(MutableMapping): # type: ignore[type-arg] + """Expands a basic dictionary with session attributes.""" + + @property + def permanent(self) -> bool: + """This reflects the ``'_permanent'`` key in the dict.""" + return self.get("_permanent", False) + + @permanent.setter + def permanent(self, value: bool) -> None: + self["_permanent"] = bool(value) + + #: Some implementations can detect whether a session is newly + #: created, but that is not guaranteed. Use with caution. The mixin + # default is hard-coded ``False``. + new = False + + #: Some implementations can detect changes to the session and set + #: this when that happens. The mixin default is hard coded to + #: ``True``. + modified = True + + #: Some implementations can detect when session data is read or + #: written and set this when that happens. The mixin default is hard + #: coded to ``True``. + accessed = True + + +# TODO generic when Python > 3.8 +class SecureCookieSession(CallbackDict, SessionMixin): # type: ignore[type-arg] + """Base class for sessions based on signed cookies. + + This session backend will set the :attr:`modified` and + :attr:`accessed` attributes. It cannot reliably track whether a + session is new (vs. empty), so :attr:`new` remains hard coded to + ``False``. + """ + + #: When data is changed, this is set to ``True``. Only the session + #: dictionary itself is tracked; if the session contains mutable + #: data (for example a nested dict) then this must be set to + #: ``True`` manually when modifying that data. The session cookie + #: will only be written to the response if this is ``True``. + modified = False + + #: When data is read or written, this is set to ``True``. Used by + # :class:`.SecureCookieSessionInterface` to add a ``Vary: Cookie`` + #: header, which allows caching proxies to cache different pages for + #: different users. + accessed = False + + def __init__(self, initial: t.Any = None) -> None: + def on_update(self: te.Self) -> None: + self.modified = True + self.accessed = True + + super().__init__(initial, on_update) + + def __getitem__(self, key: str) -> t.Any: + self.accessed = True + return super().__getitem__(key) + + def get(self, key: str, default: t.Any = None) -> t.Any: + self.accessed = True + return super().get(key, default) + + def setdefault(self, key: str, default: t.Any = None) -> t.Any: + self.accessed = True + return super().setdefault(key, default) + + +class NullSession(SecureCookieSession): + """Class used to generate nicer error messages if sessions are not + available. Will still allow read-only access to the empty session + but fail on setting. + """ + + def _fail(self, *args: t.Any, **kwargs: t.Any) -> t.NoReturn: + raise RuntimeError( + "The session is unavailable because no secret " + "key was set. Set the secret_key on the " + "application to something unique and secret." + ) + + __setitem__ = __delitem__ = clear = pop = popitem = update = setdefault = _fail # type: ignore # noqa: B950 + del _fail + + +class SessionInterface: + """The basic interface you have to implement in order to replace the + default session interface which uses werkzeug's securecookie + implementation. The only methods you have to implement are + :meth:`open_session` and :meth:`save_session`, the others have + useful defaults which you don't need to change. + + The session object returned by the :meth:`open_session` method has to + provide a dictionary like interface plus the properties and methods + from the :class:`SessionMixin`. We recommend just subclassing a dict + and adding that mixin:: + + class Session(dict, SessionMixin): + pass + + If :meth:`open_session` returns ``None`` Flask will call into + :meth:`make_null_session` to create a session that acts as replacement + if the session support cannot work because some requirement is not + fulfilled. The default :class:`NullSession` class that is created + will complain that the secret key was not set. + + To replace the session interface on an application all you have to do + is to assign :attr:`flask.Flask.session_interface`:: + + app = Flask(__name__) + app.session_interface = MySessionInterface() + + Multiple requests with the same session may be sent and handled + concurrently. When implementing a new session interface, consider + whether reads or writes to the backing store must be synchronized. + There is no guarantee on the order in which the session for each + request is opened or saved, it will occur in the order that requests + begin and end processing. + + .. versionadded:: 0.8 + """ + + #: :meth:`make_null_session` will look here for the class that should + #: be created when a null session is requested. Likewise the + #: :meth:`is_null_session` method will perform a typecheck against + #: this type. + null_session_class = NullSession + + #: A flag that indicates if the session interface is pickle based. + #: This can be used by Flask extensions to make a decision in regards + #: to how to deal with the session object. + #: + #: .. versionadded:: 0.10 + pickle_based = False + + def make_null_session(self, app: Flask) -> NullSession: + """Creates a null session which acts as a replacement object if the + real session support could not be loaded due to a configuration + error. This mainly aids the user experience because the job of the + null session is to still support lookup without complaining but + modifications are answered with a helpful error message of what + failed. + + This creates an instance of :attr:`null_session_class` by default. + """ + return self.null_session_class() + + def is_null_session(self, obj: object) -> bool: + """Checks if a given object is a null session. Null sessions are + not asked to be saved. + + This checks if the object is an instance of :attr:`null_session_class` + by default. + """ + return isinstance(obj, self.null_session_class) + + def get_cookie_name(self, app: Flask) -> str: + """The name of the session cookie. Uses``app.config["SESSION_COOKIE_NAME"]``.""" + return app.config["SESSION_COOKIE_NAME"] # type: ignore[no-any-return] + + def get_cookie_domain(self, app: Flask) -> str | None: + """The value of the ``Domain`` parameter on the session cookie. If not set, + browsers will only send the cookie to the exact domain it was set from. + Otherwise, they will send it to any subdomain of the given value as well. + + Uses the :data:`SESSION_COOKIE_DOMAIN` config. + + .. versionchanged:: 2.3 + Not set by default, does not fall back to ``SERVER_NAME``. + """ + return app.config["SESSION_COOKIE_DOMAIN"] # type: ignore[no-any-return] + + def get_cookie_path(self, app: Flask) -> str: + """Returns the path for which the cookie should be valid. The + default implementation uses the value from the ``SESSION_COOKIE_PATH`` + config var if it's set, and falls back to ``APPLICATION_ROOT`` or + uses ``/`` if it's ``None``. + """ + return app.config["SESSION_COOKIE_PATH"] or app.config["APPLICATION_ROOT"] # type: ignore[no-any-return] + + def get_cookie_httponly(self, app: Flask) -> bool: + """Returns True if the session cookie should be httponly. This + currently just returns the value of the ``SESSION_COOKIE_HTTPONLY`` + config var. + """ + return app.config["SESSION_COOKIE_HTTPONLY"] # type: ignore[no-any-return] + + def get_cookie_secure(self, app: Flask) -> bool: + """Returns True if the cookie should be secure. This currently + just returns the value of the ``SESSION_COOKIE_SECURE`` setting. + """ + return app.config["SESSION_COOKIE_SECURE"] # type: ignore[no-any-return] + + def get_cookie_samesite(self, app: Flask) -> str | None: + """Return ``'Strict'`` or ``'Lax'`` if the cookie should use the + ``SameSite`` attribute. This currently just returns the value of + the :data:`SESSION_COOKIE_SAMESITE` setting. + """ + return app.config["SESSION_COOKIE_SAMESITE"] # type: ignore[no-any-return] + + def get_expiration_time(self, app: Flask, session: SessionMixin) -> datetime | None: + """A helper method that returns an expiration date for the session + or ``None`` if the session is linked to the browser session. The + default implementation returns now + the permanent session + lifetime configured on the application. + """ + if session.permanent: + return datetime.now(timezone.utc) + app.permanent_session_lifetime + return None + + def should_set_cookie(self, app: Flask, session: SessionMixin) -> bool: + """Used by session backends to determine if a ``Set-Cookie`` header + should be set for this session cookie for this response. If the session + has been modified, the cookie is set. If the session is permanent and + the ``SESSION_REFRESH_EACH_REQUEST`` config is true, the cookie is + always set. + + This check is usually skipped if the session was deleted. + + .. versionadded:: 0.11 + """ + + return session.modified or ( + session.permanent and app.config["SESSION_REFRESH_EACH_REQUEST"] + ) + + def open_session(self, app: Flask, request: Request) -> SessionMixin | None: + """This is called at the beginning of each request, after + pushing the request context, before matching the URL. + + This must return an object which implements a dictionary-like + interface as well as the :class:`SessionMixin` interface. + + This will return ``None`` to indicate that loading failed in + some way that is not immediately an error. The request + context will fall back to using :meth:`make_null_session` + in this case. + """ + raise NotImplementedError() + + def save_session( + self, app: Flask, session: SessionMixin, response: Response + ) -> None: + """This is called at the end of each request, after generating + a response, before removing the request context. It is skipped + if :meth:`is_null_session` returns ``True``. + """ + raise NotImplementedError() + + +session_json_serializer = TaggedJSONSerializer() + + +def _lazy_sha1(string: bytes = b"") -> t.Any: + """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include + SHA-1, in which case the import and use as a default would fail before the + developer can configure something else. + """ + return hashlib.sha1(string) + + +class SecureCookieSessionInterface(SessionInterface): + """The default session interface that stores sessions in signed cookies + through the :mod:`itsdangerous` module. + """ + + #: the salt that should be applied on top of the secret key for the + #: signing of cookie based sessions. + salt = "cookie-session" + #: the hash function to use for the signature. The default is sha1 + digest_method = staticmethod(_lazy_sha1) + #: the name of the itsdangerous supported key derivation. The default + #: is hmac. + key_derivation = "hmac" + #: A python serializer for the payload. The default is a compact + #: JSON derived serializer with support for some extra Python types + #: such as datetime objects or tuples. + serializer = session_json_serializer + session_class = SecureCookieSession + + def get_signing_serializer(self, app: Flask) -> URLSafeTimedSerializer | None: + if not app.secret_key: + return None + signer_kwargs = dict( + key_derivation=self.key_derivation, digest_method=self.digest_method + ) + return URLSafeTimedSerializer( + app.secret_key, + salt=self.salt, + serializer=self.serializer, + signer_kwargs=signer_kwargs, + ) + + def open_session(self, app: Flask, request: Request) -> SecureCookieSession | None: + s = self.get_signing_serializer(app) + if s is None: + return None + val = request.cookies.get(self.get_cookie_name(app)) + if not val: + return self.session_class() + max_age = int(app.permanent_session_lifetime.total_seconds()) + try: + data = s.loads(val, max_age=max_age) + return self.session_class(data) + except BadSignature: + return self.session_class() + + def save_session( + self, app: Flask, session: SessionMixin, response: Response + ) -> None: + name = self.get_cookie_name(app) + domain = self.get_cookie_domain(app) + path = self.get_cookie_path(app) + secure = self.get_cookie_secure(app) + samesite = self.get_cookie_samesite(app) + httponly = self.get_cookie_httponly(app) + + # Add a "Vary: Cookie" header if the session was accessed at all. + if session.accessed: + response.vary.add("Cookie") + + # If the session is modified to be empty, remove the cookie. + # If the session is empty, return without setting the cookie. + if not session: + if session.modified: + response.delete_cookie( + name, + domain=domain, + path=path, + secure=secure, + samesite=samesite, + httponly=httponly, + ) + response.vary.add("Cookie") + + return + + if not self.should_set_cookie(app, session): + return + + expires = self.get_expiration_time(app, session) + val = self.get_signing_serializer(app).dumps(dict(session)) # type: ignore + response.set_cookie( + name, + val, # type: ignore + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + samesite=samesite, + ) + response.vary.add("Cookie") diff --git a/netdeploy/lib/python3.11/site-packages/flask/signals.py b/netdeploy/lib/python3.11/site-packages/flask/signals.py new file mode 100644 index 0000000..444fda9 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/signals.py @@ -0,0 +1,17 @@ +from __future__ import annotations + +from blinker import Namespace + +# This namespace is only for signals provided by Flask itself. +_signals = Namespace() + +template_rendered = _signals.signal("template-rendered") +before_render_template = _signals.signal("before-render-template") +request_started = _signals.signal("request-started") +request_finished = _signals.signal("request-finished") +request_tearing_down = _signals.signal("request-tearing-down") +got_request_exception = _signals.signal("got-request-exception") +appcontext_tearing_down = _signals.signal("appcontext-tearing-down") +appcontext_pushed = _signals.signal("appcontext-pushed") +appcontext_popped = _signals.signal("appcontext-popped") +message_flashed = _signals.signal("message-flashed") diff --git a/netdeploy/lib/python3.11/site-packages/flask/templating.py b/netdeploy/lib/python3.11/site-packages/flask/templating.py new file mode 100644 index 0000000..618a3b3 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/templating.py @@ -0,0 +1,219 @@ +from __future__ import annotations + +import typing as t + +from jinja2 import BaseLoader +from jinja2 import Environment as BaseEnvironment +from jinja2 import Template +from jinja2 import TemplateNotFound + +from .globals import _cv_app +from .globals import _cv_request +from .globals import current_app +from .globals import request +from .helpers import stream_with_context +from .signals import before_render_template +from .signals import template_rendered + +if t.TYPE_CHECKING: # pragma: no cover + from .app import Flask + from .sansio.app import App + from .sansio.scaffold import Scaffold + + +def _default_template_ctx_processor() -> dict[str, t.Any]: + """Default template context processor. Injects `request`, + `session` and `g`. + """ + appctx = _cv_app.get(None) + reqctx = _cv_request.get(None) + rv: dict[str, t.Any] = {} + if appctx is not None: + rv["g"] = appctx.g + if reqctx is not None: + rv["request"] = reqctx.request + rv["session"] = reqctx.session + return rv + + +class Environment(BaseEnvironment): + """Works like a regular Jinja2 environment but has some additional + knowledge of how Flask's blueprint works so that it can prepend the + name of the blueprint to referenced templates if necessary. + """ + + def __init__(self, app: App, **options: t.Any) -> None: + if "loader" not in options: + options["loader"] = app.create_global_jinja_loader() + BaseEnvironment.__init__(self, **options) + self.app = app + + +class DispatchingJinjaLoader(BaseLoader): + """A loader that looks for templates in the application and all + the blueprint folders. + """ + + def __init__(self, app: App) -> None: + self.app = app + + def get_source( + self, environment: BaseEnvironment, template: str + ) -> tuple[str, str | None, t.Callable[[], bool] | None]: + if self.app.config["EXPLAIN_TEMPLATE_LOADING"]: + return self._get_source_explained(environment, template) + return self._get_source_fast(environment, template) + + def _get_source_explained( + self, environment: BaseEnvironment, template: str + ) -> tuple[str, str | None, t.Callable[[], bool] | None]: + attempts = [] + rv: tuple[str, str | None, t.Callable[[], bool] | None] | None + trv: None | (tuple[str, str | None, t.Callable[[], bool] | None]) = None + + for srcobj, loader in self._iter_loaders(template): + try: + rv = loader.get_source(environment, template) + if trv is None: + trv = rv + except TemplateNotFound: + rv = None + attempts.append((loader, srcobj, rv)) + + from .debughelpers import explain_template_loading_attempts + + explain_template_loading_attempts(self.app, template, attempts) + + if trv is not None: + return trv + raise TemplateNotFound(template) + + def _get_source_fast( + self, environment: BaseEnvironment, template: str + ) -> tuple[str, str | None, t.Callable[[], bool] | None]: + for _srcobj, loader in self._iter_loaders(template): + try: + return loader.get_source(environment, template) + except TemplateNotFound: + continue + raise TemplateNotFound(template) + + def _iter_loaders(self, template: str) -> t.Iterator[tuple[Scaffold, BaseLoader]]: + loader = self.app.jinja_loader + if loader is not None: + yield self.app, loader + + for blueprint in self.app.iter_blueprints(): + loader = blueprint.jinja_loader + if loader is not None: + yield blueprint, loader + + def list_templates(self) -> list[str]: + result = set() + loader = self.app.jinja_loader + if loader is not None: + result.update(loader.list_templates()) + + for blueprint in self.app.iter_blueprints(): + loader = blueprint.jinja_loader + if loader is not None: + for template in loader.list_templates(): + result.add(template) + + return list(result) + + +def _render(app: Flask, template: Template, context: dict[str, t.Any]) -> str: + app.update_template_context(context) + before_render_template.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + rv = template.render(context) + template_rendered.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + return rv + + +def render_template( + template_name_or_list: str | Template | list[str | Template], + **context: t.Any, +) -> str: + """Render a template by name with the given context. + + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _render(app, template, context) + + +def render_template_string(source: str, **context: t.Any) -> str: + """Render a template from the given source string with the given + context. + + :param source: The source code of the template to render. + :param context: The variables to make available in the template. + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _render(app, template, context) + + +def _stream( + app: Flask, template: Template, context: dict[str, t.Any] +) -> t.Iterator[str]: + app.update_template_context(context) + before_render_template.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + + def generate() -> t.Iterator[str]: + yield from template.generate(context) + template_rendered.send( + app, _async_wrapper=app.ensure_sync, template=template, context=context + ) + + rv = generate() + + # If a request context is active, keep it while generating. + if request: + rv = stream_with_context(rv) + + return rv + + +def stream_template( + template_name_or_list: str | Template | list[str | Template], + **context: t.Any, +) -> t.Iterator[str]: + """Render a template by name with the given context as a stream. + This returns an iterator of strings, which can be used as a + streaming response from a view. + + :param template_name_or_list: The name of the template to render. If + a list is given, the first name to exist will be rendered. + :param context: The variables to make available in the template. + + .. versionadded:: 2.2 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.get_or_select_template(template_name_or_list) + return _stream(app, template, context) + + +def stream_template_string(source: str, **context: t.Any) -> t.Iterator[str]: + """Render a template from the given source string with the given + context as a stream. This returns an iterator of strings, which can + be used as a streaming response from a view. + + :param source: The source code of the template to render. + :param context: The variables to make available in the template. + + .. versionadded:: 2.2 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + template = app.jinja_env.from_string(source) + return _stream(app, template, context) diff --git a/netdeploy/lib/python3.11/site-packages/flask/testing.py b/netdeploy/lib/python3.11/site-packages/flask/testing.py new file mode 100644 index 0000000..a27b7c8 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/testing.py @@ -0,0 +1,298 @@ +from __future__ import annotations + +import importlib.metadata +import typing as t +from contextlib import contextmanager +from contextlib import ExitStack +from copy import copy +from types import TracebackType +from urllib.parse import urlsplit + +import werkzeug.test +from click.testing import CliRunner +from werkzeug.test import Client +from werkzeug.wrappers import Request as BaseRequest + +from .cli import ScriptInfo +from .sessions import SessionMixin + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import WSGIEnvironment + from werkzeug.test import TestResponse + + from .app import Flask + + +class EnvironBuilder(werkzeug.test.EnvironBuilder): + """An :class:`~werkzeug.test.EnvironBuilder`, that takes defaults from the + application. + + :param app: The Flask application to configure the environment from. + :param path: URL path being requested. + :param base_url: Base URL where the app is being served, which + ``path`` is relative to. If not given, built from + :data:`PREFERRED_URL_SCHEME`, ``subdomain``, + :data:`SERVER_NAME`, and :data:`APPLICATION_ROOT`. + :param subdomain: Subdomain name to append to :data:`SERVER_NAME`. + :param url_scheme: Scheme to use instead of + :data:`PREFERRED_URL_SCHEME`. + :param json: If given, this is serialized as JSON and passed as + ``data``. Also defaults ``content_type`` to + ``application/json``. + :param args: other positional arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + :param kwargs: other keyword arguments passed to + :class:`~werkzeug.test.EnvironBuilder`. + """ + + def __init__( + self, + app: Flask, + path: str = "/", + base_url: str | None = None, + subdomain: str | None = None, + url_scheme: str | None = None, + *args: t.Any, + **kwargs: t.Any, + ) -> None: + assert not (base_url or subdomain or url_scheme) or ( + base_url is not None + ) != bool( + subdomain or url_scheme + ), 'Cannot pass "subdomain" or "url_scheme" with "base_url".' + + if base_url is None: + http_host = app.config.get("SERVER_NAME") or "localhost" + app_root = app.config["APPLICATION_ROOT"] + + if subdomain: + http_host = f"{subdomain}.{http_host}" + + if url_scheme is None: + url_scheme = app.config["PREFERRED_URL_SCHEME"] + + url = urlsplit(path) + base_url = ( + f"{url.scheme or url_scheme}://{url.netloc or http_host}" + f"/{app_root.lstrip('/')}" + ) + path = url.path + + if url.query: + sep = b"?" if isinstance(url.query, bytes) else "?" + path += sep + url.query + + self.app = app + super().__init__(path, base_url, *args, **kwargs) + + def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: # type: ignore + """Serialize ``obj`` to a JSON-formatted string. + + The serialization will be configured according to the config associated + with this EnvironBuilder's ``app``. + """ + return self.app.json.dumps(obj, **kwargs) + + +_werkzeug_version = "" + + +def _get_werkzeug_version() -> str: + global _werkzeug_version + + if not _werkzeug_version: + _werkzeug_version = importlib.metadata.version("werkzeug") + + return _werkzeug_version + + +class FlaskClient(Client): + """Works like a regular Werkzeug test client but has knowledge about + Flask's contexts to defer the cleanup of the request context until + the end of a ``with`` block. For general information about how to + use this class refer to :class:`werkzeug.test.Client`. + + .. versionchanged:: 0.12 + `app.test_client()` includes preset default environment, which can be + set after instantiation of the `app.test_client()` object in + `client.environ_base`. + + Basic usage is outlined in the :doc:`/testing` chapter. + """ + + application: Flask + + def __init__(self, *args: t.Any, **kwargs: t.Any) -> None: + super().__init__(*args, **kwargs) + self.preserve_context = False + self._new_contexts: list[t.ContextManager[t.Any]] = [] + self._context_stack = ExitStack() + self.environ_base = { + "REMOTE_ADDR": "127.0.0.1", + "HTTP_USER_AGENT": f"Werkzeug/{_get_werkzeug_version()}", + } + + @contextmanager + def session_transaction( + self, *args: t.Any, **kwargs: t.Any + ) -> t.Iterator[SessionMixin]: + """When used in combination with a ``with`` statement this opens a + session transaction. This can be used to modify the session that + the test client uses. Once the ``with`` block is left the session is + stored back. + + :: + + with client.session_transaction() as session: + session['value'] = 42 + + Internally this is implemented by going through a temporary test + request context and since session handling could depend on + request variables this function accepts the same arguments as + :meth:`~flask.Flask.test_request_context` which are directly + passed through. + """ + if self._cookies is None: + raise TypeError( + "Cookies are disabled. Create a client with 'use_cookies=True'." + ) + + app = self.application + ctx = app.test_request_context(*args, **kwargs) + self._add_cookies_to_wsgi(ctx.request.environ) + + with ctx: + sess = app.session_interface.open_session(app, ctx.request) + + if sess is None: + raise RuntimeError("Session backend did not open a session.") + + yield sess + resp = app.response_class() + + if app.session_interface.is_null_session(sess): + return + + with ctx: + app.session_interface.save_session(app, sess, resp) + + self._update_cookies_from_response( + ctx.request.host.partition(":")[0], + ctx.request.path, + resp.headers.getlist("Set-Cookie"), + ) + + def _copy_environ(self, other: WSGIEnvironment) -> WSGIEnvironment: + out = {**self.environ_base, **other} + + if self.preserve_context: + out["werkzeug.debug.preserve_context"] = self._new_contexts.append + + return out + + def _request_from_builder_args( + self, args: tuple[t.Any, ...], kwargs: dict[str, t.Any] + ) -> BaseRequest: + kwargs["environ_base"] = self._copy_environ(kwargs.get("environ_base", {})) + builder = EnvironBuilder(self.application, *args, **kwargs) + + try: + return builder.get_request() + finally: + builder.close() + + def open( + self, + *args: t.Any, + buffered: bool = False, + follow_redirects: bool = False, + **kwargs: t.Any, + ) -> TestResponse: + if args and isinstance( + args[0], (werkzeug.test.EnvironBuilder, dict, BaseRequest) + ): + if isinstance(args[0], werkzeug.test.EnvironBuilder): + builder = copy(args[0]) + builder.environ_base = self._copy_environ(builder.environ_base or {}) # type: ignore[arg-type] + request = builder.get_request() + elif isinstance(args[0], dict): + request = EnvironBuilder.from_environ( + args[0], app=self.application, environ_base=self._copy_environ({}) + ).get_request() + else: + # isinstance(args[0], BaseRequest) + request = copy(args[0]) + request.environ = self._copy_environ(request.environ) + else: + # request is None + request = self._request_from_builder_args(args, kwargs) + + # Pop any previously preserved contexts. This prevents contexts + # from being preserved across redirects or multiple requests + # within a single block. + self._context_stack.close() + + response = super().open( + request, + buffered=buffered, + follow_redirects=follow_redirects, + ) + response.json_module = self.application.json # type: ignore[assignment] + + # Re-push contexts that were preserved during the request. + while self._new_contexts: + cm = self._new_contexts.pop() + self._context_stack.enter_context(cm) + + return response + + def __enter__(self) -> FlaskClient: + if self.preserve_context: + raise RuntimeError("Cannot nest client invocations") + self.preserve_context = True + return self + + def __exit__( + self, + exc_type: type | None, + exc_value: BaseException | None, + tb: TracebackType | None, + ) -> None: + self.preserve_context = False + self._context_stack.close() + + +class FlaskCliRunner(CliRunner): + """A :class:`~click.testing.CliRunner` for testing a Flask app's + CLI commands. Typically created using + :meth:`~flask.Flask.test_cli_runner`. See :ref:`testing-cli`. + """ + + def __init__(self, app: Flask, **kwargs: t.Any) -> None: + self.app = app + super().__init__(**kwargs) + + def invoke( # type: ignore + self, cli: t.Any = None, args: t.Any = None, **kwargs: t.Any + ) -> t.Any: + """Invokes a CLI command in an isolated environment. See + :meth:`CliRunner.invoke ` for + full method documentation. See :ref:`testing-cli` for examples. + + If the ``obj`` argument is not given, passes an instance of + :class:`~flask.cli.ScriptInfo` that knows how to load the Flask + app being tested. + + :param cli: Command object to invoke. Default is the app's + :attr:`~flask.app.Flask.cli` group. + :param args: List of strings to invoke the command with. + + :return: a :class:`~click.testing.Result` object. + """ + if cli is None: + cli = self.app.cli + + if "obj" not in kwargs: + kwargs["obj"] = ScriptInfo(create_app=lambda: self.app) + + return super().invoke(cli, args, **kwargs) diff --git a/netdeploy/lib/python3.11/site-packages/flask/typing.py b/netdeploy/lib/python3.11/site-packages/flask/typing.py new file mode 100644 index 0000000..cf6d4ae --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/typing.py @@ -0,0 +1,90 @@ +from __future__ import annotations + +import typing as t + +if t.TYPE_CHECKING: # pragma: no cover + from _typeshed.wsgi import WSGIApplication # noqa: F401 + from werkzeug.datastructures import Headers # noqa: F401 + from werkzeug.sansio.response import Response # noqa: F401 + +# The possible types that are directly convertible or are a Response object. +ResponseValue = t.Union[ + "Response", + str, + bytes, + t.List[t.Any], + # Only dict is actually accepted, but Mapping allows for TypedDict. + t.Mapping[str, t.Any], + t.Iterator[str], + t.Iterator[bytes], +] + +# the possible types for an individual HTTP header +# This should be a Union, but mypy doesn't pass unless it's a TypeVar. +HeaderValue = t.Union[str, t.List[str], t.Tuple[str, ...]] + +# the possible types for HTTP headers +HeadersValue = t.Union[ + "Headers", + t.Mapping[str, HeaderValue], + t.Sequence[t.Tuple[str, HeaderValue]], +] + +# The possible types returned by a route function. +ResponseReturnValue = t.Union[ + ResponseValue, + t.Tuple[ResponseValue, HeadersValue], + t.Tuple[ResponseValue, int], + t.Tuple[ResponseValue, int, HeadersValue], + "WSGIApplication", +] + +# Allow any subclass of werkzeug.Response, such as the one from Flask, +# as a callback argument. Using werkzeug.Response directly makes a +# callback annotated with flask.Response fail type checking. +ResponseClass = t.TypeVar("ResponseClass", bound="Response") + +AppOrBlueprintKey = t.Optional[str] # The App key is None, whereas blueprints are named +AfterRequestCallable = t.Union[ + t.Callable[[ResponseClass], ResponseClass], + t.Callable[[ResponseClass], t.Awaitable[ResponseClass]], +] +BeforeFirstRequestCallable = t.Union[ + t.Callable[[], None], t.Callable[[], t.Awaitable[None]] +] +BeforeRequestCallable = t.Union[ + t.Callable[[], t.Optional[ResponseReturnValue]], + t.Callable[[], t.Awaitable[t.Optional[ResponseReturnValue]]], +] +ShellContextProcessorCallable = t.Callable[[], t.Dict[str, t.Any]] +TeardownCallable = t.Union[ + t.Callable[[t.Optional[BaseException]], None], + t.Callable[[t.Optional[BaseException]], t.Awaitable[None]], +] +TemplateContextProcessorCallable = t.Union[ + t.Callable[[], t.Dict[str, t.Any]], + t.Callable[[], t.Awaitable[t.Dict[str, t.Any]]], +] +TemplateFilterCallable = t.Callable[..., t.Any] +TemplateGlobalCallable = t.Callable[..., t.Any] +TemplateTestCallable = t.Callable[..., bool] +URLDefaultCallable = t.Callable[[str, t.Dict[str, t.Any]], None] +URLValuePreprocessorCallable = t.Callable[ + [t.Optional[str], t.Optional[t.Dict[str, t.Any]]], None +] + +# This should take Exception, but that either breaks typing the argument +# with a specific exception, or decorating multiple times with different +# exceptions (and using a union type on the argument). +# https://github.com/pallets/flask/issues/4095 +# https://github.com/pallets/flask/issues/4295 +# https://github.com/pallets/flask/issues/4297 +ErrorHandlerCallable = t.Union[ + t.Callable[[t.Any], ResponseReturnValue], + t.Callable[[t.Any], t.Awaitable[ResponseReturnValue]], +] + +RouteCallable = t.Union[ + t.Callable[..., ResponseReturnValue], + t.Callable[..., t.Awaitable[ResponseReturnValue]], +] diff --git a/netdeploy/lib/python3.11/site-packages/flask/views.py b/netdeploy/lib/python3.11/site-packages/flask/views.py new file mode 100644 index 0000000..794fdc0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/views.py @@ -0,0 +1,191 @@ +from __future__ import annotations + +import typing as t + +from . import typing as ft +from .globals import current_app +from .globals import request + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + +http_method_funcs = frozenset( + ["get", "post", "head", "options", "delete", "put", "trace", "patch"] +) + + +class View: + """Subclass this class and override :meth:`dispatch_request` to + create a generic class-based view. Call :meth:`as_view` to create a + view function that creates an instance of the class with the given + arguments and calls its ``dispatch_request`` method with any URL + variables. + + See :doc:`views` for a detailed guide. + + .. code-block:: python + + class Hello(View): + init_every_request = False + + def dispatch_request(self, name): + return f"Hello, {name}!" + + app.add_url_rule( + "/hello/", view_func=Hello.as_view("hello") + ) + + Set :attr:`methods` on the class to change what methods the view + accepts. + + Set :attr:`decorators` on the class to apply a list of decorators to + the generated view function. Decorators applied to the class itself + will not be applied to the generated view function! + + Set :attr:`init_every_request` to ``False`` for efficiency, unless + you need to store request-global data on ``self``. + """ + + #: The methods this view is registered for. Uses the same default + #: (``["GET", "HEAD", "OPTIONS"]``) as ``route`` and + #: ``add_url_rule`` by default. + methods: t.ClassVar[t.Collection[str] | None] = None + + #: Control whether the ``OPTIONS`` method is handled automatically. + #: Uses the same default (``True``) as ``route`` and + #: ``add_url_rule`` by default. + provide_automatic_options: t.ClassVar[bool | None] = None + + #: A list of decorators to apply, in order, to the generated view + #: function. Remember that ``@decorator`` syntax is applied bottom + #: to top, so the first decorator in the list would be the bottom + #: decorator. + #: + #: .. versionadded:: 0.8 + decorators: t.ClassVar[list[t.Callable[[F], F]]] = [] + + #: Create a new instance of this view class for every request by + #: default. If a view subclass sets this to ``False``, the same + #: instance is used for every request. + #: + #: A single instance is more efficient, especially if complex setup + #: is done during init. However, storing data on ``self`` is no + #: longer safe across requests, and :data:`~flask.g` should be used + #: instead. + #: + #: .. versionadded:: 2.2 + init_every_request: t.ClassVar[bool] = True + + def dispatch_request(self) -> ft.ResponseReturnValue: + """The actual view function behavior. Subclasses must override + this and return a valid response. Any variables from the URL + rule are passed as keyword arguments. + """ + raise NotImplementedError() + + @classmethod + def as_view( + cls, name: str, *class_args: t.Any, **class_kwargs: t.Any + ) -> ft.RouteCallable: + """Convert the class into a view function that can be registered + for a route. + + By default, the generated view will create a new instance of the + view class for every request and call its + :meth:`dispatch_request` method. If the view class sets + :attr:`init_every_request` to ``False``, the same instance will + be used for every request. + + Except for ``name``, all other arguments passed to this method + are forwarded to the view class ``__init__`` method. + + .. versionchanged:: 2.2 + Added the ``init_every_request`` class attribute. + """ + if cls.init_every_request: + + def view(**kwargs: t.Any) -> ft.ResponseReturnValue: + self = view.view_class( # type: ignore[attr-defined] + *class_args, **class_kwargs + ) + return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] + + else: + self = cls(*class_args, **class_kwargs) + + def view(**kwargs: t.Any) -> ft.ResponseReturnValue: + return current_app.ensure_sync(self.dispatch_request)(**kwargs) # type: ignore[no-any-return] + + if cls.decorators: + view.__name__ = name + view.__module__ = cls.__module__ + for decorator in cls.decorators: + view = decorator(view) + + # We attach the view class to the view function for two reasons: + # first of all it allows us to easily figure out what class-based + # view this thing came from, secondly it's also used for instantiating + # the view class so you can actually replace it with something else + # for testing purposes and debugging. + view.view_class = cls # type: ignore + view.__name__ = name + view.__doc__ = cls.__doc__ + view.__module__ = cls.__module__ + view.methods = cls.methods # type: ignore + view.provide_automatic_options = cls.provide_automatic_options # type: ignore + return view + + +class MethodView(View): + """Dispatches request methods to the corresponding instance methods. + For example, if you implement a ``get`` method, it will be used to + handle ``GET`` requests. + + This can be useful for defining a REST API. + + :attr:`methods` is automatically set based on the methods defined on + the class. + + See :doc:`views` for a detailed guide. + + .. code-block:: python + + class CounterAPI(MethodView): + def get(self): + return str(session.get("counter", 0)) + + def post(self): + session["counter"] = session.get("counter", 0) + 1 + return redirect(url_for("counter")) + + app.add_url_rule( + "/counter", view_func=CounterAPI.as_view("counter") + ) + """ + + def __init_subclass__(cls, **kwargs: t.Any) -> None: + super().__init_subclass__(**kwargs) + + if "methods" not in cls.__dict__: + methods = set() + + for base in cls.__bases__: + if getattr(base, "methods", None): + methods.update(base.methods) # type: ignore[attr-defined] + + for key in http_method_funcs: + if hasattr(cls, key): + methods.add(key.upper()) + + if methods: + cls.methods = methods + + def dispatch_request(self, **kwargs: t.Any) -> ft.ResponseReturnValue: + meth = getattr(self, request.method.lower(), None) + + # If the request method is HEAD and we don't have a handler for it + # retry with GET. + if meth is None and request.method == "HEAD": + meth = getattr(self, "get", None) + + assert meth is not None, f"Unimplemented method {request.method!r}" + return current_app.ensure_sync(meth)(**kwargs) # type: ignore[no-any-return] diff --git a/netdeploy/lib/python3.11/site-packages/flask/wrappers.py b/netdeploy/lib/python3.11/site-packages/flask/wrappers.py new file mode 100644 index 0000000..c1eca80 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/flask/wrappers.py @@ -0,0 +1,174 @@ +from __future__ import annotations + +import typing as t + +from werkzeug.exceptions import BadRequest +from werkzeug.exceptions import HTTPException +from werkzeug.wrappers import Request as RequestBase +from werkzeug.wrappers import Response as ResponseBase + +from . import json +from .globals import current_app +from .helpers import _split_blueprint_path + +if t.TYPE_CHECKING: # pragma: no cover + from werkzeug.routing import Rule + + +class Request(RequestBase): + """The request object used by default in Flask. Remembers the + matched endpoint and view arguments. + + It is what ends up as :class:`~flask.request`. If you want to replace + the request object used you can subclass this and set + :attr:`~flask.Flask.request_class` to your subclass. + + The request object is a :class:`~werkzeug.wrappers.Request` subclass and + provides all of the attributes Werkzeug defines plus a few Flask + specific ones. + """ + + json_module: t.Any = json + + #: The internal URL rule that matched the request. This can be + #: useful to inspect which methods are allowed for the URL from + #: a before/after handler (``request.url_rule.methods``) etc. + #: Though if the request's method was invalid for the URL rule, + #: the valid list is available in ``routing_exception.valid_methods`` + #: instead (an attribute of the Werkzeug exception + #: :exc:`~werkzeug.exceptions.MethodNotAllowed`) + #: because the request was never internally bound. + #: + #: .. versionadded:: 0.6 + url_rule: Rule | None = None + + #: A dict of view arguments that matched the request. If an exception + #: happened when matching, this will be ``None``. + view_args: dict[str, t.Any] | None = None + + #: If matching the URL failed, this is the exception that will be + #: raised / was raised as part of the request handling. This is + #: usually a :exc:`~werkzeug.exceptions.NotFound` exception or + #: something similar. + routing_exception: HTTPException | None = None + + @property + def max_content_length(self) -> int | None: # type: ignore[override] + """Read-only view of the ``MAX_CONTENT_LENGTH`` config key.""" + if current_app: + return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return] + else: + return None + + @property + def endpoint(self) -> str | None: + """The endpoint that matched the request URL. + + This will be ``None`` if matching failed or has not been + performed yet. + + This in combination with :attr:`view_args` can be used to + reconstruct the same URL or a modified URL. + """ + if self.url_rule is not None: + return self.url_rule.endpoint + + return None + + @property + def blueprint(self) -> str | None: + """The registered name of the current blueprint. + + This will be ``None`` if the endpoint is not part of a + blueprint, or if URL matching failed or has not been performed + yet. + + This does not necessarily match the name the blueprint was + created with. It may have been nested, or registered with a + different name. + """ + endpoint = self.endpoint + + if endpoint is not None and "." in endpoint: + return endpoint.rpartition(".")[0] + + return None + + @property + def blueprints(self) -> list[str]: + """The registered names of the current blueprint upwards through + parent blueprints. + + This will be an empty list if there is no current blueprint, or + if URL matching failed. + + .. versionadded:: 2.0.1 + """ + name = self.blueprint + + if name is None: + return [] + + return _split_blueprint_path(name) + + def _load_form_data(self) -> None: + super()._load_form_data() + + # In debug mode we're replacing the files multidict with an ad-hoc + # subclass that raises a different error for key errors. + if ( + current_app + and current_app.debug + and self.mimetype != "multipart/form-data" + and not self.files + ): + from .debughelpers import attach_enctype_error_multidict + + attach_enctype_error_multidict(self) + + def on_json_loading_failed(self, e: ValueError | None) -> t.Any: + try: + return super().on_json_loading_failed(e) + except BadRequest as e: + if current_app and current_app.debug: + raise + + raise BadRequest() from e + + +class Response(ResponseBase): + """The response object that is used by default in Flask. Works like the + response object from Werkzeug but is set to have an HTML mimetype by + default. Quite often you don't have to create this object yourself because + :meth:`~flask.Flask.make_response` will take care of that for you. + + If you want to replace the response object used you can subclass this and + set :attr:`~flask.Flask.response_class` to your subclass. + + .. versionchanged:: 1.0 + JSON support is added to the response, like the request. This is useful + when testing to get the test client response data as JSON. + + .. versionchanged:: 1.0 + + Added :attr:`max_cookie_size`. + """ + + default_mimetype: str | None = "text/html" + + json_module = json + + autocorrect_location_header = False + + @property + def max_cookie_size(self) -> int: # type: ignore + """Read-only view of the :data:`MAX_COOKIE_SIZE` config key. + + See :attr:`~werkzeug.wrappers.Response.max_cookie_size` in + Werkzeug's docs. + """ + if current_app: + return current_app.config["MAX_COOKIE_SIZE"] # type: ignore[no-any-return] + + # return Werkzeug's default when not in an app context + return super().max_cookie_size diff --git a/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/INSTALLER b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/METADATA b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/METADATA new file mode 100644 index 0000000..0e3a649 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/METADATA @@ -0,0 +1,117 @@ +Metadata-Version: 2.4 +Name: greenlet +Version: 3.2.4 +Summary: Lightweight in-process concurrent programming +Home-page: https://greenlet.readthedocs.io/ +Author: Alexey Borzenkov +Author-email: snaury@gmail.com +Maintainer: Jason Madden +Maintainer-email: jason@seecoresoftware.com +License: MIT AND Python-2.0 +Project-URL: Bug Tracker, https://github.com/python-greenlet/greenlet/issues +Project-URL: Source Code, https://github.com/python-greenlet/greenlet/ +Project-URL: Documentation, https://greenlet.readthedocs.io/ +Project-URL: Changes, https://greenlet.readthedocs.io/en/latest/changes.html +Keywords: greenlet coroutine concurrency threads cooperative +Platform: any +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: Natural Language :: English +Classifier: Programming Language :: C +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +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: Programming Language :: Python :: 3.13 +Classifier: Operating System :: OS Independent +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.9 +Description-Content-Type: text/x-rst +License-File: LICENSE +License-File: LICENSE.PSF +Provides-Extra: docs +Requires-Dist: Sphinx; extra == "docs" +Requires-Dist: furo; extra == "docs" +Provides-Extra: test +Requires-Dist: objgraph; extra == "test" +Requires-Dist: psutil; extra == "test" +Requires-Dist: setuptools; extra == "test" +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: description-content-type +Dynamic: home-page +Dynamic: keywords +Dynamic: license +Dynamic: license-file +Dynamic: maintainer +Dynamic: maintainer-email +Dynamic: platform +Dynamic: project-url +Dynamic: provides-extra +Dynamic: requires-python +Dynamic: summary + +.. This file is included into docs/history.rst + + +Greenlets are lightweight coroutines for in-process concurrent +programming. + +The "greenlet" package is a spin-off of `Stackless`_, a version of +CPython that supports micro-threads called "tasklets". Tasklets run +pseudo-concurrently (typically in a single or a few OS-level threads) +and are synchronized with data exchanges on "channels". + +A "greenlet", on the other hand, is a still more primitive notion of +micro-thread with no implicit scheduling; coroutines, in other words. +This is useful when you want to control exactly when your code runs. +You can build custom scheduled micro-threads on top of greenlet; +however, it seems that greenlets are useful on their own as a way to +make advanced control flow structures. For example, we can recreate +generators; the difference with Python's own generators is that our +generators can call nested functions and the nested functions can +yield values too. (Additionally, you don't need a "yield" keyword. See +the example in `test_generator.py +`_). + +Greenlets are provided as a C extension module for the regular unmodified +interpreter. + +.. _`Stackless`: http://www.stackless.com + + +Who is using Greenlet? +====================== + +There are several libraries that use Greenlet as a more flexible +alternative to Python's built in coroutine support: + + - `Concurrence`_ + - `Eventlet`_ + - `Gevent`_ + +.. _Concurrence: http://opensource.hyves.org/concurrence/ +.. _Eventlet: http://eventlet.net/ +.. _Gevent: http://www.gevent.org/ + +Getting Greenlet +================ + +The easiest way to get Greenlet is to install it with pip:: + + pip install greenlet + + +Source code archives and binary distributions are available on the +python package index at https://pypi.org/project/greenlet + +The source code repository is hosted on github: +https://github.com/python-greenlet/greenlet + +Documentation is available on readthedocs.org: +https://greenlet.readthedocs.io diff --git a/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/RECORD b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/RECORD new file mode 100644 index 0000000..2f1fe6e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/RECORD @@ -0,0 +1,121 @@ +../../../include/site/python3.11/greenlet/greenlet.h,sha256=sz5pYRSQqedgOt2AMgxLZdTjO-qcr_JMvgiEJR9IAJ8,4755 +greenlet-3.2.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +greenlet-3.2.4.dist-info/METADATA,sha256=ZwuiD2PER_KIrBSuuQdUPtK-VCLKtfY5RueYGQheX6o,4120 +greenlet-3.2.4.dist-info/RECORD,, +greenlet-3.2.4.dist-info/WHEEL,sha256=N6PyfvHGx46Sh1ny6KlB0rtGwHkXZAwlLCEEPBiTPn8,152 +greenlet-3.2.4.dist-info/licenses/LICENSE,sha256=dpgx1uXfrywggC-sz_H6-0wgJd2PYlPfpH_K1Z1NCXk,1434 +greenlet-3.2.4.dist-info/licenses/LICENSE.PSF,sha256=5f88I8EQ5JTNfXNsEP2W1GJFe6_soxCEDbZScpjH1Gs,2424 +greenlet-3.2.4.dist-info/top_level.txt,sha256=YSnRsCRoO61JGlP57o8iKL6rdLWDWuiyKD8ekpWUsDc,9 +greenlet/CObjects.cpp,sha256=OPej1bWBgc4sRrTRQ2aFFML9pzDYKlKhlJSjsI0X_eU,3508 +greenlet/PyGreenlet.cpp,sha256=dGal9uux_E0d6yMaZfVYpdD9x1XFVOrp4s_or_D_UEM,24199 +greenlet/PyGreenlet.hpp,sha256=2ZQlOxYNoy7QwD7mppFoOXe_At56NIsJ0eNsE_hoSsw,1463 +greenlet/PyGreenletUnswitchable.cpp,sha256=PQE0fSZa_IOyUM44IESHkJoD2KtGW3dkhkmZSYY3WHs,4375 +greenlet/PyModule.cpp,sha256=J2TH06dGcNEarioS6NbWXkdME8hJY05XVbdqLrfO5w4,8587 +greenlet/TBrokenGreenlet.cpp,sha256=smN26uC7ahAbNYiS10rtWPjCeTG4jevM8siA2sjJiXg,1021 +greenlet/TExceptionState.cpp,sha256=U7Ctw9fBdNraS0d174MoQW7bN-ae209Ta0JuiKpcpVI,1359 +greenlet/TGreenlet.cpp,sha256=IM4cHsv1drEl35d7n8YOA_wR-R7oRvx5XhOJOK2PBB8,25732 +greenlet/TGreenlet.hpp,sha256=DoN795i3vofgll-20GA-ylg3qCNw-nKprLA6r7CK5HY,28522 +greenlet/TGreenletGlobals.cpp,sha256=YyEmDjKf1g32bsL-unIUScFLnnA1fzLWf2gOMd-D0Zw,3264 +greenlet/TMainGreenlet.cpp,sha256=fvgb8HHB-FVTPEKjR1s_ifCZSpp5D5YQByik0CnIABg,3276 +greenlet/TPythonState.cpp,sha256=b12U09sNjQvKG0_agROFHuJkDDa7HDccWaFW55XViQA,15975 +greenlet/TStackState.cpp,sha256=V444I8Jj9DhQz-9leVW_9dtiSRjaE1NMlgDG02Xxq-Y,7381 +greenlet/TThreadState.hpp,sha256=2Jgg7DtGggMYR_x3CLAvAFf1mIdIDtQvSSItcdmX4ZQ,19131 +greenlet/TThreadStateCreator.hpp,sha256=uYTexDWooXSSgUc5uh-Mhm5BQi3-kR6CqpizvNynBFQ,2610 +greenlet/TThreadStateDestroy.cpp,sha256=36yBCAMq3beXTZd-XnFA7DwaHVSOx2vc28-nf0spysU,8169 +greenlet/TUserGreenlet.cpp,sha256=uemg0lwKXtYB0yzmvyYdIIAsKnNkifXM1OJ2OlrFP1A,23553 +greenlet/__init__.py,sha256=vSR8EU6Bi32-0MkAlx--fzCL-Eheh6EqJWa-7B9LTOk,1723 +greenlet/__pycache__/__init__.cpython-311.pyc,, +greenlet/_greenlet.cpython-311-x86_64-linux-gnu.so,sha256=TkjvWEnGAXpCQgzzry0_iDHyP40sVXMVuRhT4lj8xTM,1365232 +greenlet/greenlet.cpp,sha256=WdItb1yWL9WNsTqJNf0Iw8ZwDHD49pkDP0rIRGBg2pw,10996 +greenlet/greenlet.h,sha256=sz5pYRSQqedgOt2AMgxLZdTjO-qcr_JMvgiEJR9IAJ8,4755 +greenlet/greenlet_allocator.hpp,sha256=eC0S1AQuep1vnVRsag-r83xgfAtbpn0qQZ-oXzQXaso,2607 +greenlet/greenlet_compiler_compat.hpp,sha256=nRxpLN9iNbnLVyFDeVmOwyeeNm6scQrOed1l7JQYMCM,4346 +greenlet/greenlet_cpython_compat.hpp,sha256=kJG6d_yDwwl3bSZOOFqM3ks1UzVIGcwbsTM2s8C6VYE,4149 +greenlet/greenlet_exceptions.hpp,sha256=06Bx81DtVaJTa6RtiMcV141b-XHv4ppEgVItkblcLWY,4503 +greenlet/greenlet_internal.hpp,sha256=Ajc-_09W4xWzm9XfyXHAeQAFUgKGKsnJwYsTCoNy3ns,2709 +greenlet/greenlet_msvc_compat.hpp,sha256=0MyaiyoCE_A6UROXZlMQRxRS17gfyh0d7NUppU3EVFc,2978 +greenlet/greenlet_refs.hpp,sha256=OnbA91yZf3QHH6-eJccvoNDAaN-pQBMMrclFU1Ot3J4,34436 +greenlet/greenlet_slp_switch.hpp,sha256=kM1QHA2iV-gH4cFyN6lfIagHQxvJZjWOVJdIxRE3TlQ,3198 +greenlet/greenlet_thread_support.hpp,sha256=XUJ6ljWjf9OYyuOILiz8e_yHvT3fbaUiHdhiPNQUV4s,867 +greenlet/platform/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +greenlet/platform/__pycache__/__init__.cpython-311.pyc,, +greenlet/platform/setup_switch_x64_masm.cmd,sha256=ZpClUJeU0ujEPSTWNSepP0W2f9XiYQKA8QKSoVou8EU,143 +greenlet/platform/switch_aarch64_gcc.h,sha256=GKC0yWNXnbK2X--X6aguRCMj2Tg7hDU1Zkl3RljDvC8,4307 +greenlet/platform/switch_alpha_unix.h,sha256=Z-SvF8JQV3oxWT8JRbL9RFu4gRFxPdJ7cviM8YayMmw,671 +greenlet/platform/switch_amd64_unix.h,sha256=EcSFCBlodEBhqhKjcJqY_5Dn_jn7pKpkJlOvp7gFXLI,2748 +greenlet/platform/switch_arm32_gcc.h,sha256=Z3KkHszdgq6uU4YN3BxvKMG2AdDnovwCCNrqGWZ1Lyo,2479 +greenlet/platform/switch_arm32_ios.h,sha256=mm5_R9aXB92hyxzFRwB71M60H6AlvHjrpTrc72Pz3l8,1892 +greenlet/platform/switch_arm64_masm.asm,sha256=4kpTtfy7rfcr8j1CpJLAK21EtZpGDAJXWRU68HEy5A8,1245 +greenlet/platform/switch_arm64_masm.obj,sha256=DmLnIB_icoEHAz1naue_pJPTZgR9ElM7-Nmztr-o9_U,746 +greenlet/platform/switch_arm64_msvc.h,sha256=RqK5MHLmXI3Q-FQ7tm32KWnbDNZKnkJdq8CR89cz640,398 +greenlet/platform/switch_csky_gcc.h,sha256=kDikyiPpewP71KoBZQO_MukDTXTXBiC7x-hF0_2DL0w,1331 +greenlet/platform/switch_loongarch64_linux.h,sha256=7M-Dhc4Q8tRbJCJhalDLwU6S9Mx8MjmN1RbTDgIvQTM,779 +greenlet/platform/switch_m68k_gcc.h,sha256=VSa6NpZhvyyvF-Q58CTIWSpEDo4FKygOyTz00whctlw,928 +greenlet/platform/switch_mips_unix.h,sha256=E0tYsqc5anDY1BhenU1l8DW-nVHC_BElzLgJw3TGtPk,1426 +greenlet/platform/switch_ppc64_aix.h,sha256=_BL0iyRr3ZA5iPlr3uk9SJ5sNRWGYLrXcZ5z-CE9anE,3860 +greenlet/platform/switch_ppc64_linux.h,sha256=0rriT5XyxPb0GqsSSn_bP9iQsnjsPbBmu0yqo5goSyQ,3815 +greenlet/platform/switch_ppc_aix.h,sha256=pHA4slEjUFP3J3SYm1TAlNPhgb2G_PAtax5cO8BEe1A,2941 +greenlet/platform/switch_ppc_linux.h,sha256=YwrlKUzxlXuiKMQqr6MFAV1bPzWnmvk6X1AqJZEpOWU,2759 +greenlet/platform/switch_ppc_macosx.h,sha256=Z6KN_ud0n6nC3ltJrNz2qtvER6vnRAVRNH9mdIDpMxY,2624 +greenlet/platform/switch_ppc_unix.h,sha256=-ZG7MSSPEA5N4qO9PQChtyEJ-Fm6qInhyZm_ZBHTtMg,2652 +greenlet/platform/switch_riscv_unix.h,sha256=606V6ACDf79Fz_WGItnkgbjIJ0pGg_sHmPyDxQYKK58,949 +greenlet/platform/switch_s390_unix.h,sha256=RRlGu957ybmq95qNNY4Qw1mcaoT3eBnW5KbVwu48KX8,2763 +greenlet/platform/switch_sh_gcc.h,sha256=mcRJBTu-2UBf4kZtX601qofwuDuy-Y-hnxJtrcaB7do,901 +greenlet/platform/switch_sparc_sun_gcc.h,sha256=xZish9GsMHBienUbUMsX1-ZZ-as7hs36sVhYIE3ew8Y,2797 +greenlet/platform/switch_x32_unix.h,sha256=nM98PKtzTWc1lcM7TRMUZJzskVdR1C69U1UqZRWX0GE,1509 +greenlet/platform/switch_x64_masm.asm,sha256=nu6n2sWyXuXfpPx40d9YmLfHXUc1sHgeTvX1kUzuvEM,1841 +greenlet/platform/switch_x64_masm.obj,sha256=GNtTNxYdo7idFUYsQv-mrXWgyT5EJ93-9q90lN6svtQ,1078 +greenlet/platform/switch_x64_msvc.h,sha256=LIeasyKo_vHzspdMzMHbosRhrBfKI4BkQOh4qcTHyJw,1805 +greenlet/platform/switch_x86_msvc.h,sha256=TtGOwinbFfnn6clxMNkCz8i6OmgB6kVRrShoF5iT9to,12838 +greenlet/platform/switch_x86_unix.h,sha256=VplW9H0FF0cZHw1DhJdIUs5q6YLS4cwb2nYwjF83R1s,3059 +greenlet/slp_platformselect.h,sha256=hTb3GFdcPUYJTuu1MY93js7MZEax1_e5E-gflpi0RzI,3959 +greenlet/tests/__init__.py,sha256=EtTtQfpRDde0MhsdAM5Cm7LYIfS_HKUIFwquiH4Q7ac,9736 +greenlet/tests/__pycache__/__init__.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_clearing_run_switches.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_cpp_exception.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_initialstub_already_started.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_slp_switch.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_switch_three_greenlets.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_switch_three_greenlets2.cpython-311.pyc,, +greenlet/tests/__pycache__/fail_switch_two_greenlets.cpython-311.pyc,, +greenlet/tests/__pycache__/leakcheck.cpython-311.pyc,, +greenlet/tests/__pycache__/test_contextvars.cpython-311.pyc,, +greenlet/tests/__pycache__/test_cpp.cpython-311.pyc,, +greenlet/tests/__pycache__/test_extension_interface.cpython-311.pyc,, +greenlet/tests/__pycache__/test_gc.cpython-311.pyc,, +greenlet/tests/__pycache__/test_generator.cpython-311.pyc,, +greenlet/tests/__pycache__/test_generator_nested.cpython-311.pyc,, +greenlet/tests/__pycache__/test_greenlet.cpython-311.pyc,, +greenlet/tests/__pycache__/test_greenlet_trash.cpython-311.pyc,, +greenlet/tests/__pycache__/test_leaks.cpython-311.pyc,, +greenlet/tests/__pycache__/test_stack_saved.cpython-311.pyc,, +greenlet/tests/__pycache__/test_throw.cpython-311.pyc,, +greenlet/tests/__pycache__/test_tracing.cpython-311.pyc,, +greenlet/tests/__pycache__/test_version.cpython-311.pyc,, +greenlet/tests/__pycache__/test_weakref.cpython-311.pyc,, +greenlet/tests/_test_extension.c,sha256=vkeGA-6oeJcGILsD7oIrT1qZop2GaTOHXiNT7mcSl-0,5773 +greenlet/tests/_test_extension.cpython-311-x86_64-linux-gnu.so,sha256=p118NJ4hObhSNcvKLduspwQExvXHPDAbWVVMU6o3dqs,17256 +greenlet/tests/_test_extension_cpp.cpp,sha256=e0kVnaB8CCaEhE9yHtNyfqTjevsPDKKx-zgxk7PPK48,6565 +greenlet/tests/_test_extension_cpp.cpython-311-x86_64-linux-gnu.so,sha256=oY-c-ycRV67QTFu7qSj83Uf-XU91QUPv7oqQ4Yd3YF0,57920 +greenlet/tests/fail_clearing_run_switches.py,sha256=o433oA_nUCtOPaMEGc8VEhZIKa71imVHXFw7TsXaP8M,1263 +greenlet/tests/fail_cpp_exception.py,sha256=o_ZbipWikok8Bjc-vjiQvcb5FHh2nVW-McGKMLcMzh0,985 +greenlet/tests/fail_initialstub_already_started.py,sha256=txENn5IyzGx2p-XR1XB7qXmC8JX_4mKDEA8kYBXUQKc,1961 +greenlet/tests/fail_slp_switch.py,sha256=rJBZcZfTWR3e2ERQtPAud6YKShiDsP84PmwOJbp4ey0,524 +greenlet/tests/fail_switch_three_greenlets.py,sha256=zSitV7rkNnaoHYVzAGGLnxz-yPtohXJJzaE8ehFDQ0M,956 +greenlet/tests/fail_switch_three_greenlets2.py,sha256=FPJensn2EJxoropl03JSTVP3kgP33k04h6aDWWozrOk,1285 +greenlet/tests/fail_switch_two_greenlets.py,sha256=1CaI8s3504VbbF1vj1uBYuy-zxBHVzHPIAd1LIc8ONg,817 +greenlet/tests/leakcheck.py,sha256=JHgc45bnTyVtn9MiprIlz2ygSXMFtcaCSp2eB9XIhQE,12612 +greenlet/tests/test_contextvars.py,sha256=xutO-qZgKTwKsA9lAqTjIcTBEiQV4RpNKM-vO2_YCVU,10541 +greenlet/tests/test_cpp.py,sha256=hpxhFAdKJTpAVZP8CBGs1ZcrKdscI9BaDZk4btkI5d4,2736 +greenlet/tests/test_extension_interface.py,sha256=eJ3cwLacdK2WbsrC-4DgeyHdwLRcG4zx7rrkRtqSzC4,3829 +greenlet/tests/test_gc.py,sha256=PCOaRpIyjNnNlDogGL3FZU_lrdXuM-pv1rxeE5TP5mc,2923 +greenlet/tests/test_generator.py,sha256=tONXiTf98VGm347o1b-810daPiwdla5cbpFg6QI1R1g,1240 +greenlet/tests/test_generator_nested.py,sha256=7v4HOYrf1XZP39dk5IUMubdZ8yc3ynwZcqj9GUJyMSA,3718 +greenlet/tests/test_greenlet.py,sha256=gSG6hOjKYyRRe5ZzNUpskrUcMnBT3WU4yITTzaZfLH4,47995 +greenlet/tests/test_greenlet_trash.py,sha256=n2dBlQfOoEO1ODatFi8QdhboH3fB86YtqzcYMYOXxbw,7947 +greenlet/tests/test_leaks.py,sha256=OFSE870Zyql85HukfC_XYa2c4gDQBU889RV1AlLum74,18076 +greenlet/tests/test_stack_saved.py,sha256=eyzqNY2VCGuGlxhT_In6TvZ6Okb0AXFZVyBEnK1jDwA,446 +greenlet/tests/test_throw.py,sha256=u2TQ_WvvCd6N6JdXWIxVEcXkKu5fepDlz9dktYdmtng,3712 +greenlet/tests/test_tracing.py,sha256=NFD6Vcww8grBnFQFhCNdswwGetjLeLQ7vL2Qqw3LWBM,8591 +greenlet/tests/test_version.py,sha256=O9DpAITsOFgiRcjd4odQ7ejmwx_N9Q1zQENVcbtFHIc,1339 +greenlet/tests/test_weakref.py,sha256=F8M23btEF87bIbpptLNBORosbQqNZGiYeKMqYjWrsak,883 diff --git a/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/WHEEL b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/WHEEL new file mode 100644 index 0000000..283ae68 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/WHEEL @@ -0,0 +1,6 @@ +Wheel-Version: 1.0 +Generator: setuptools (80.9.0) +Root-Is-Purelib: false +Tag: cp311-cp311-manylinux_2_24_x86_64 +Tag: cp311-cp311-manylinux_2_28_x86_64 + diff --git a/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE new file mode 100644 index 0000000..b73a4a1 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE @@ -0,0 +1,30 @@ +The following files are derived from Stackless Python and are subject to the +same license as Stackless Python: + + src/greenlet/slp_platformselect.h + files in src/greenlet/platform/ directory + +See LICENSE.PSF and http://www.stackless.com/ for details. + +Unless otherwise noted, the files in greenlet have been released under the +following MIT license: + +Copyright (c) Armin Rigo, Christian Tismer and 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/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE.PSF b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE.PSF new file mode 100644 index 0000000..d3b509a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE.PSF @@ -0,0 +1,47 @@ +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011 Python Software Foundation; All Rights Reserved" are retained in Python +alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. diff --git a/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/top_level.txt b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/top_level.txt new file mode 100644 index 0000000..46725be --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/top_level.txt @@ -0,0 +1 @@ +greenlet diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/CObjects.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/CObjects.cpp new file mode 100644 index 0000000..c135995 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/CObjects.cpp @@ -0,0 +1,157 @@ +#ifndef COBJECTS_CPP +#define COBJECTS_CPP +/***************************************************************************** + * C interface + * + * These are exported using the CObject API + */ +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +#endif + +#include "greenlet_exceptions.hpp" + +#include "greenlet_internal.hpp" +#include "greenlet_refs.hpp" + + +#include "TThreadStateDestroy.cpp" + +#include "PyGreenlet.hpp" + +using greenlet::PyErrOccurred; +using greenlet::Require; + + + +extern "C" { +static PyGreenlet* +PyGreenlet_GetCurrent(void) +{ + return GET_THREAD_STATE().state().get_current().relinquish_ownership(); +} + +static int +PyGreenlet_SetParent(PyGreenlet* g, PyGreenlet* nparent) +{ + return green_setparent((PyGreenlet*)g, (PyObject*)nparent, NULL); +} + +static PyGreenlet* +PyGreenlet_New(PyObject* run, PyGreenlet* parent) +{ + using greenlet::refs::NewDictReference; + // In the past, we didn't use green_new and green_init, but that + // was a maintenance issue because we duplicated code. This way is + // much safer, but slightly slower. If that's a problem, we could + // refactor green_init to separate argument parsing from initialization. + OwnedGreenlet g = OwnedGreenlet::consuming(green_new(&PyGreenlet_Type, nullptr, nullptr)); + if (!g) { + return NULL; + } + + try { + NewDictReference kwargs; + if (run) { + kwargs.SetItem(mod_globs->str_run, run); + } + if (parent) { + kwargs.SetItem("parent", (PyObject*)parent); + } + + Require(green_init(g.borrow(), mod_globs->empty_tuple, kwargs.borrow())); + } + catch (const PyErrOccurred&) { + return nullptr; + } + + return g.relinquish_ownership(); +} + +static PyObject* +PyGreenlet_Switch(PyGreenlet* self, PyObject* args, PyObject* kwargs) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return NULL; + } + + if (args == NULL) { + args = mod_globs->empty_tuple; + } + + if (kwargs == NULL || !PyDict_Check(kwargs)) { + kwargs = NULL; + } + + return green_switch(self, args, kwargs); +} + +static PyObject* +PyGreenlet_Throw(PyGreenlet* self, PyObject* typ, PyObject* val, PyObject* tb) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return nullptr; + } + try { + PyErrPieces err_pieces(typ, val, tb); + return internal_green_throw(self, err_pieces).relinquish_ownership(); + } + catch (const PyErrOccurred&) { + return nullptr; + } +} + + + +static int +Extern_PyGreenlet_MAIN(PyGreenlet* self) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return -1; + } + return self->pimpl->main(); +} + +static int +Extern_PyGreenlet_ACTIVE(PyGreenlet* self) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return -1; + } + return self->pimpl->active(); +} + +static int +Extern_PyGreenlet_STARTED(PyGreenlet* self) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return -1; + } + return self->pimpl->started(); +} + +static PyGreenlet* +Extern_PyGreenlet_GET_PARENT(PyGreenlet* self) +{ + if (!PyGreenlet_Check(self)) { + PyErr_BadArgument(); + return NULL; + } + // This can return NULL even if there is no exception + return self->pimpl->parent().acquire(); +} +} // extern C. + +/** End C API ****************************************************************/ +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/PyGreenlet.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/PyGreenlet.cpp new file mode 100644 index 0000000..6b118a5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/PyGreenlet.cpp @@ -0,0 +1,751 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +#ifndef PYGREENLET_CPP +#define PYGREENLET_CPP +/***************** +The Python slot functions for TGreenlet. + */ + + +#define PY_SSIZE_T_CLEAN +#include +#include "structmember.h" // PyMemberDef + +#include "greenlet_internal.hpp" +#include "TThreadStateDestroy.cpp" +#include "TGreenlet.hpp" +// #include "TUserGreenlet.cpp" +// #include "TMainGreenlet.cpp" +// #include "TBrokenGreenlet.cpp" + + +#include "greenlet_refs.hpp" +#include "greenlet_slp_switch.hpp" + +#include "greenlet_thread_support.hpp" +#include "TGreenlet.hpp" + +#include "TGreenletGlobals.cpp" +#include "TThreadStateDestroy.cpp" +#include "PyGreenlet.hpp" +// #include "TGreenlet.cpp" + +// #include "TExceptionState.cpp" +// #include "TPythonState.cpp" +// #include "TStackState.cpp" + +using greenlet::LockGuard; +using greenlet::LockInitError; +using greenlet::PyErrOccurred; +using greenlet::Require; + +using greenlet::g_handle_exit; +using greenlet::single_result; + +using greenlet::Greenlet; +using greenlet::UserGreenlet; +using greenlet::MainGreenlet; +using greenlet::BrokenGreenlet; +using greenlet::ThreadState; +using greenlet::PythonState; + + + +static PyGreenlet* +green_new(PyTypeObject* type, PyObject* UNUSED(args), PyObject* UNUSED(kwds)) +{ + PyGreenlet* o = + (PyGreenlet*)PyBaseObject_Type.tp_new(type, mod_globs->empty_tuple, mod_globs->empty_dict); + if (o) { + // Recall: borrowing or getting the current greenlet + // causes the "deleteme list" to get cleared. So constructing a greenlet + // can do things like cause other greenlets to get finalized. + UserGreenlet* c = new UserGreenlet(o, GET_THREAD_STATE().state().borrow_current()); + assert(Py_REFCNT(o) == 1); + // Also: This looks like a memory leak, but isn't. Constructing the + // C++ object assigns it to the pimpl pointer of the Python object (o); + // we'll need that later. + assert(c == o->pimpl); + } + return o; +} + + +// green_init is used in the tp_init slot. So it's important that +// it can be called directly from CPython. Thus, we don't use +// BorrowedGreenlet and BorrowedObject --- although in theory +// these should be binary layout compatible, that may not be +// guaranteed to be the case (32-bit linux ppc possibly). +static int +green_init(PyGreenlet* self, PyObject* args, PyObject* kwargs) +{ + PyArgParseParam run; + PyArgParseParam nparent; + static const char* kwlist[] = { + "run", + "parent", + NULL + }; + + // recall: The O specifier does NOT increase the reference count. + if (!PyArg_ParseTupleAndKeywords( + args, kwargs, "|OO:green", (char**)kwlist, &run, &nparent)) { + return -1; + } + + if (run) { + if (green_setrun(self, run, NULL)) { + return -1; + } + } + if (nparent && !nparent.is_None()) { + return green_setparent(self, nparent, NULL); + } + return 0; +} + + + +static int +green_traverse(PyGreenlet* self, visitproc visit, void* arg) +{ + // We must only visit referenced objects, i.e. only objects + // Py_INCREF'ed by this greenlet (directly or indirectly): + // + // - stack_prev is not visited: holds previous stack pointer, but it's not + // referenced + // - frames are not visited as we don't strongly reference them; + // alive greenlets are not garbage collected + // anyway. This can be a problem, however, if this greenlet is + // never allowed to finish, and is referenced from the frame: we + // have an uncollectible cycle in that case. Note that the + // frame object itself is also frequently not even tracked by the GC + // starting with Python 3.7 (frames are allocated by the + // interpreter untracked, and only become tracked when their + // evaluation is finished if they have a refcount > 1). All of + // this is to say that we should probably strongly reference + // the frame object. Doing so, while always allowing GC on a + // greenlet, solves several leaks for us. + + Py_VISIT(self->dict); + if (!self->pimpl) { + // Hmm. I have seen this at interpreter shutdown time, + // I think. That's very odd because this doesn't go away until + // we're ``green_dealloc()``, at which point we shouldn't be + // traversed anymore. + return 0; + } + + return self->pimpl->tp_traverse(visit, arg); +} + +static int +green_is_gc(PyObject* _self) +{ + BorrowedGreenlet self(_self); + int result = 0; + /* Main greenlet can be garbage collected since it can only + become unreachable if the underlying thread exited. + Active greenlets --- including those that are suspended --- + cannot be garbage collected, however. + */ + if (self->main() || !self->active()) { + result = 1; + } + // The main greenlet pointer will eventually go away after the thread dies. + if (self->was_running_in_dead_thread()) { + // Our thread is dead! We can never run again. Might as well + // GC us. Note that if a tuple containing only us and other + // immutable objects had been scanned before this, when we + // would have returned 0, the tuple will take itself out of GC + // tracking and never be investigated again. So that could + // result in both us and the tuple leaking due to an + // unreachable/uncollectible reference. The same goes for + // dictionaries. + // + // It's not a great idea to be changing our GC state on the + // fly. + result = 1; + } + return result; +} + + +static int +green_clear(PyGreenlet* self) +{ + /* Greenlet is only cleared if it is about to be collected. + Since active greenlets are not garbage collectable, we can + be sure that, even if they are deallocated during clear, + nothing they reference is in unreachable or finalizers, + so even if it switches we are relatively safe. */ + // XXX: Are we responsible for clearing weakrefs here? + Py_CLEAR(self->dict); + return self->pimpl->tp_clear(); +} + +/** + * Returns 0 on failure (the object was resurrected) or 1 on success. + **/ +static int +_green_dealloc_kill_started_non_main_greenlet(BorrowedGreenlet self) +{ + /* Hacks hacks hacks copied from instance_dealloc() */ + /* Temporarily resurrect the greenlet. */ + assert(self.REFCNT() == 0); + Py_SET_REFCNT(self.borrow(), 1); + /* Save the current exception, if any. */ + PyErrPieces saved_err; + try { + // BY THE TIME WE GET HERE, the state may actually be going + // away + // if we're shutting down the interpreter and freeing thread + // entries, + // this could result in freeing greenlets that were leaked. So + // we can't try to read the state. + self->deallocing_greenlet_in_thread( + self->thread_state() + ? static_cast(GET_THREAD_STATE()) + : nullptr); + } + catch (const PyErrOccurred&) { + PyErr_WriteUnraisable(self.borrow_o()); + /* XXX what else should we do? */ + } + /* Check for no resurrection must be done while we keep + * our internal reference, otherwise PyFile_WriteObject + * causes recursion if using Py_INCREF/Py_DECREF + */ + if (self.REFCNT() == 1 && self->active()) { + /* Not resurrected, but still not dead! + XXX what else should we do? we complain. */ + PyObject* f = PySys_GetObject("stderr"); + Py_INCREF(self.borrow_o()); /* leak! */ + if (f != NULL) { + PyFile_WriteString("GreenletExit did not kill ", f); + PyFile_WriteObject(self.borrow_o(), f, 0); + PyFile_WriteString("\n", f); + } + } + /* Restore the saved exception. */ + saved_err.PyErrRestore(); + /* Undo the temporary resurrection; can't use DECREF here, + * it would cause a recursive call. + */ + assert(self.REFCNT() > 0); + + Py_ssize_t refcnt = self.REFCNT() - 1; + Py_SET_REFCNT(self.borrow_o(), refcnt); + if (refcnt != 0) { + /* Resurrected! */ + _Py_NewReference(self.borrow_o()); + Py_SET_REFCNT(self.borrow_o(), refcnt); + /* Better to use tp_finalizer slot (PEP 442) + * and call ``PyObject_CallFinalizerFromDealloc``, + * but that's only supported in Python 3.4+; see + * Modules/_io/iobase.c for an example. + * TODO: We no longer run on anything that old, switch to finalizers. + * + * The following approach is copied from iobase.c in CPython 2.7. + * (along with much of this function in general). Here's their + * comment: + * + * When called from a heap type's dealloc, the type will be + * decref'ed on return (see e.g. subtype_dealloc in typeobject.c). + * + * On free-threaded builds of CPython, the type is meant to be immortal + * so we probably shouldn't mess with this? See + * test_issue_245_reference_counting_subclass_no_threads + */ + if (PyType_HasFeature(self.TYPE(), Py_TPFLAGS_HEAPTYPE)) { + Py_INCREF(self.TYPE()); + } + + PyObject_GC_Track((PyObject*)self); + + GREENLET_Py_DEC_REFTOTAL; +#ifdef COUNT_ALLOCS + --Py_TYPE(self)->tp_frees; + --Py_TYPE(self)->tp_allocs; +#endif /* COUNT_ALLOCS */ + return 0; + } + return 1; +} + + +static void +green_dealloc(PyGreenlet* self) +{ + PyObject_GC_UnTrack(self); + BorrowedGreenlet me(self); + if (me->active() + && me->started() + && !me->main()) { + if (!_green_dealloc_kill_started_non_main_greenlet(me)) { + return; + } + } + + if (self->weakreflist != NULL) { + PyObject_ClearWeakRefs((PyObject*)self); + } + Py_CLEAR(self->dict); + + if (self->pimpl) { + // In case deleting this, which frees some memory, + // somehow winds up calling back into us. That's usually a + //bug in our code. + Greenlet* p = self->pimpl; + self->pimpl = nullptr; + delete p; + } + // and finally we're done. self is now invalid. + Py_TYPE(self)->tp_free((PyObject*)self); +} + + + +static OwnedObject +internal_green_throw(BorrowedGreenlet self, PyErrPieces& err_pieces) +{ + PyObject* result = nullptr; + err_pieces.PyErrRestore(); + assert(PyErr_Occurred()); + if (self->started() && !self->active()) { + /* dead greenlet: turn GreenletExit into a regular return */ + result = g_handle_exit(OwnedObject()).relinquish_ownership(); + } + self->args() <<= result; + + return single_result(self->g_switch()); +} + + + +PyDoc_STRVAR( + green_switch_doc, + "switch(*args, **kwargs)\n" + "\n" + "Switch execution to this greenlet.\n" + "\n" + "If this greenlet has never been run, then this greenlet\n" + "will be switched to using the body of ``self.run(*args, **kwargs)``.\n" + "\n" + "If the greenlet is active (has been run, but was switch()'ed\n" + "out before leaving its run function), then this greenlet will\n" + "be resumed and the return value to its switch call will be\n" + "None if no arguments are given, the given argument if one\n" + "argument is given, or the args tuple and keyword args dict if\n" + "multiple arguments are given.\n" + "\n" + "If the greenlet is dead, or is the current greenlet then this\n" + "function will simply return the arguments using the same rules as\n" + "above.\n"); + +static PyObject* +green_switch(PyGreenlet* self, PyObject* args, PyObject* kwargs) +{ + using greenlet::SwitchingArgs; + SwitchingArgs switch_args(OwnedObject::owning(args), OwnedObject::owning(kwargs)); + self->pimpl->may_switch_away(); + self->pimpl->args() <<= switch_args; + + // If we're switching out of a greenlet, and that switch is the + // last thing the greenlet does, the greenlet ought to be able to + // go ahead and die at that point. Currently, someone else must + // manually switch back to the greenlet so that we "fall off the + // end" and can perform cleanup. You'd think we'd be able to + // figure out that this is happening using the frame's ``f_lasti`` + // member, which is supposed to be an index into + // ``frame->f_code->co_code``, the bytecode string. However, in + // recent interpreters, ``f_lasti`` tends not to be updated thanks + // to things like the PREDICT() macros in ceval.c. So it doesn't + // really work to do that in many cases. For example, the Python + // code: + // def run(): + // greenlet.getcurrent().parent.switch() + // produces bytecode of len 16, with the actual call to switch() + // being at index 10 (in Python 3.10). However, the reported + // ``f_lasti`` we actually see is...5! (Which happens to be the + // second byte of the CALL_METHOD op for ``getcurrent()``). + + try { + //OwnedObject result = single_result(self->pimpl->g_switch()); + OwnedObject result(single_result(self->pimpl->g_switch())); +#ifndef NDEBUG + // Note that the current greenlet isn't necessarily self. If self + // finished, we went to one of its parents. + assert(!self->pimpl->args()); + + const BorrowedGreenlet& current = GET_THREAD_STATE().state().borrow_current(); + // It's possible it's never been switched to. + assert(!current->args()); +#endif + PyObject* p = result.relinquish_ownership(); + + if (!p && !PyErr_Occurred()) { + // This shouldn't be happening anymore, so the asserts + // are there for debug builds. Non-debug builds + // crash "gracefully" in this case, although there is an + // argument to be made for killing the process in all + // cases --- for this to be the case, our switches + // probably nested in an incorrect way, so the state is + // suspicious. Nothing should be corrupt though, just + // confused at the Python level. Letting this propagate is + // probably good enough. + assert(p || PyErr_Occurred()); + throw PyErrOccurred( + mod_globs->PyExc_GreenletError, + "Greenlet.switch() returned NULL without an exception set." + ); + } + return p; + } + catch(const PyErrOccurred&) { + return nullptr; + } +} + +PyDoc_STRVAR( + green_throw_doc, + "Switches execution to this greenlet, but immediately raises the\n" + "given exception in this greenlet. If no argument is provided, the " + "exception\n" + "defaults to `greenlet.GreenletExit`. The normal exception\n" + "propagation rules apply, as described for `switch`. Note that calling " + "this\n" + "method is almost equivalent to the following::\n" + "\n" + " def raiser():\n" + " raise typ, val, tb\n" + " g_raiser = greenlet(raiser, parent=g)\n" + " g_raiser.switch()\n" + "\n" + "except that this trick does not work for the\n" + "`greenlet.GreenletExit` exception, which would not propagate\n" + "from ``g_raiser`` to ``g``.\n"); + +static PyObject* +green_throw(PyGreenlet* self, PyObject* args) +{ + PyArgParseParam typ(mod_globs->PyExc_GreenletExit); + PyArgParseParam val; + PyArgParseParam tb; + + if (!PyArg_ParseTuple(args, "|OOO:throw", &typ, &val, &tb)) { + return nullptr; + } + + assert(typ.borrow() || val.borrow()); + + self->pimpl->may_switch_away(); + try { + // Both normalizing the error and the actual throw_greenlet + // could throw PyErrOccurred. + PyErrPieces err_pieces(typ.borrow(), val.borrow(), tb.borrow()); + + return internal_green_throw(self, err_pieces).relinquish_ownership(); + } + catch (const PyErrOccurred&) { + return nullptr; + } +} + +static int +green_bool(PyGreenlet* self) +{ + return self->pimpl->active(); +} + +/** + * CAUTION: Allocates memory, may run GC and arbitrary Python code. + */ +static PyObject* +green_getdict(PyGreenlet* self, void* UNUSED(context)) +{ + if (self->dict == NULL) { + self->dict = PyDict_New(); + if (self->dict == NULL) { + return NULL; + } + } + Py_INCREF(self->dict); + return self->dict; +} + +static int +green_setdict(PyGreenlet* self, PyObject* val, void* UNUSED(context)) +{ + PyObject* tmp; + + if (val == NULL) { + PyErr_SetString(PyExc_TypeError, "__dict__ may not be deleted"); + return -1; + } + if (!PyDict_Check(val)) { + PyErr_SetString(PyExc_TypeError, "__dict__ must be a dictionary"); + return -1; + } + tmp = self->dict; + Py_INCREF(val); + self->dict = val; + Py_XDECREF(tmp); + return 0; +} + +static bool +_green_not_dead(BorrowedGreenlet self) +{ + // XXX: Where else should we do this? + // Probably on entry to most Python-facing functions? + if (self->was_running_in_dead_thread()) { + self->deactivate_and_free(); + return false; + } + return self->active() || !self->started(); +} + + +static PyObject* +green_getdead(PyGreenlet* self, void* UNUSED(context)) +{ + if (_green_not_dead(self)) { + Py_RETURN_FALSE; + } + else { + Py_RETURN_TRUE; + } +} + +static PyObject* +green_get_stack_saved(PyGreenlet* self, void* UNUSED(context)) +{ + return PyLong_FromSsize_t(self->pimpl->stack_saved()); +} + + +static PyObject* +green_getrun(PyGreenlet* self, void* UNUSED(context)) +{ + try { + OwnedObject result(BorrowedGreenlet(self)->run()); + return result.relinquish_ownership(); + } + catch(const PyErrOccurred&) { + return nullptr; + } +} + + +static int +green_setrun(PyGreenlet* self, PyObject* nrun, void* UNUSED(context)) +{ + try { + BorrowedGreenlet(self)->run(nrun); + return 0; + } + catch(const PyErrOccurred&) { + return -1; + } +} + +static PyObject* +green_getparent(PyGreenlet* self, void* UNUSED(context)) +{ + return BorrowedGreenlet(self)->parent().acquire_or_None(); +} + + +static int +green_setparent(PyGreenlet* self, PyObject* nparent, void* UNUSED(context)) +{ + try { + BorrowedGreenlet(self)->parent(nparent); + } + catch(const PyErrOccurred&) { + return -1; + } + return 0; +} + + +static PyObject* +green_getcontext(const PyGreenlet* self, void* UNUSED(context)) +{ + const Greenlet *const g = self->pimpl; + try { + OwnedObject result(g->context()); + return result.relinquish_ownership(); + } + catch(const PyErrOccurred&) { + return nullptr; + } +} + +static int +green_setcontext(PyGreenlet* self, PyObject* nctx, void* UNUSED(context)) +{ + try { + BorrowedGreenlet(self)->context(nctx); + return 0; + } + catch(const PyErrOccurred&) { + return -1; + } +} + + +static PyObject* +green_getframe(PyGreenlet* self, void* UNUSED(context)) +{ + const PythonState::OwnedFrame& top_frame = BorrowedGreenlet(self)->top_frame(); + return top_frame.acquire_or_None(); +} + + +static PyObject* +green_getstate(PyGreenlet* self) +{ + PyErr_Format(PyExc_TypeError, + "cannot serialize '%s' object", + Py_TYPE(self)->tp_name); + return nullptr; +} + +static PyObject* +green_repr(PyGreenlet* _self) +{ + BorrowedGreenlet self(_self); + /* + Return a string like + + + The handling of greenlets across threads is not super good. + We mostly use the internal definitions of these terms, but they + generally should make sense to users as well. + */ + PyObject* result; + int never_started = !self->started() && !self->active(); + + const char* const tp_name = Py_TYPE(self)->tp_name; + + if (_green_not_dead(self)) { + /* XXX: The otid= is almost useless because you can't correlate it to + any thread identifier exposed to Python. We could use + PyThreadState_GET()->thread_id, but we'd need to save that in the + greenlet, or save the whole PyThreadState object itself. + + As it stands, its only useful for identifying greenlets from the same thread. + */ + const char* state_in_thread; + if (self->was_running_in_dead_thread()) { + // The thread it was running in is dead! + // This can happen, especially at interpreter shut down. + // It complicates debugging output because it may be + // impossible to access the current thread state at that + // time. Thus, don't access the current thread state. + state_in_thread = " (thread exited)"; + } + else { + state_in_thread = GET_THREAD_STATE().state().is_current(self) + ? " current" + : (self->started() ? " suspended" : ""); + } + result = PyUnicode_FromFormat( + "<%s object at %p (otid=%p)%s%s%s%s>", + tp_name, + self.borrow_o(), + self->thread_state(), + state_in_thread, + self->active() ? " active" : "", + never_started ? " pending" : " started", + self->main() ? " main" : "" + ); + } + else { + result = PyUnicode_FromFormat( + "<%s object at %p (otid=%p) %sdead>", + tp_name, + self.borrow_o(), + self->thread_state(), + self->was_running_in_dead_thread() + ? "(thread exited) " + : "" + ); + } + + return result; +} + + +static PyMethodDef green_methods[] = { + { + .ml_name="switch", + .ml_meth=reinterpret_cast(green_switch), + .ml_flags=METH_VARARGS | METH_KEYWORDS, + .ml_doc=green_switch_doc + }, + {.ml_name="throw", .ml_meth=(PyCFunction)green_throw, .ml_flags=METH_VARARGS, .ml_doc=green_throw_doc}, + {.ml_name="__getstate__", .ml_meth=(PyCFunction)green_getstate, .ml_flags=METH_NOARGS, .ml_doc=NULL}, + {.ml_name=NULL, .ml_meth=NULL} /* sentinel */ +}; + +static PyGetSetDef green_getsets[] = { + /* name, getter, setter, doc, context pointer */ + {.name="__dict__", .get=(getter)green_getdict, .set=(setter)green_setdict}, + {.name="run", .get=(getter)green_getrun, .set=(setter)green_setrun}, + {.name="parent", .get=(getter)green_getparent, .set=(setter)green_setparent}, + {.name="gr_frame", .get=(getter)green_getframe }, + { + .name="gr_context", + .get=(getter)green_getcontext, + .set=(setter)green_setcontext + }, + {.name="dead", .get=(getter)green_getdead}, + {.name="_stack_saved", .get=(getter)green_get_stack_saved}, + {.name=NULL} +}; + +static PyMemberDef green_members[] = { + {.name=NULL} +}; + +static PyNumberMethods green_as_number = { + .nb_bool=(inquiry)green_bool, +}; + + +PyTypeObject PyGreenlet_Type = { + .ob_base=PyVarObject_HEAD_INIT(NULL, 0) + .tp_name="greenlet.greenlet", /* tp_name */ + .tp_basicsize=sizeof(PyGreenlet), /* tp_basicsize */ + /* methods */ + .tp_dealloc=(destructor)green_dealloc, /* tp_dealloc */ + .tp_repr=(reprfunc)green_repr, /* tp_repr */ + .tp_as_number=&green_as_number, /* tp_as _number*/ + .tp_flags=G_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + .tp_doc="greenlet(run=None, parent=None) -> greenlet\n\n" + "Creates a new greenlet object (without running it).\n\n" + " - *run* -- The callable to invoke.\n" + " - *parent* -- The parent greenlet. The default is the current " + "greenlet.", /* tp_doc */ + .tp_traverse=(traverseproc)green_traverse, /* tp_traverse */ + .tp_clear=(inquiry)green_clear, /* tp_clear */ + .tp_weaklistoffset=offsetof(PyGreenlet, weakreflist), /* tp_weaklistoffset */ + + .tp_methods=green_methods, /* tp_methods */ + .tp_members=green_members, /* tp_members */ + .tp_getset=green_getsets, /* tp_getset */ + .tp_dictoffset=offsetof(PyGreenlet, dict), /* tp_dictoffset */ + .tp_init=(initproc)green_init, /* tp_init */ + .tp_alloc=PyType_GenericAlloc, /* tp_alloc */ + .tp_new=(newfunc)green_new, /* tp_new */ + .tp_free=PyObject_GC_Del, /* tp_free */ + .tp_is_gc=(inquiry)green_is_gc, /* tp_is_gc */ +}; + +#endif + +// Local Variables: +// flycheck-clang-include-path: ("/opt/local/Library/Frameworks/Python.framework/Versions/3.8/include/python3.8") +// End: diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/PyGreenlet.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/PyGreenlet.hpp new file mode 100644 index 0000000..df6cd80 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/PyGreenlet.hpp @@ -0,0 +1,35 @@ +#ifndef PYGREENLET_HPP +#define PYGREENLET_HPP + + +#include "greenlet.h" +#include "greenlet_compiler_compat.hpp" +#include "greenlet_refs.hpp" + + +using greenlet::refs::OwnedGreenlet; +using greenlet::refs::BorrowedGreenlet; +using greenlet::refs::BorrowedObject;; +using greenlet::refs::OwnedObject; +using greenlet::refs::PyErrPieces; + + +// XXX: These doesn't really belong here, it's not a Python slot. +static OwnedObject internal_green_throw(BorrowedGreenlet self, PyErrPieces& err_pieces); + +static PyGreenlet* green_new(PyTypeObject* type, PyObject* UNUSED(args), PyObject* UNUSED(kwds)); +static int green_clear(PyGreenlet* self); +static int green_init(PyGreenlet* self, PyObject* args, PyObject* kwargs); +static int green_setparent(PyGreenlet* self, PyObject* nparent, void* UNUSED(context)); +static int green_setrun(PyGreenlet* self, PyObject* nrun, void* UNUSED(context)); +static int green_traverse(PyGreenlet* self, visitproc visit, void* arg); +static void green_dealloc(PyGreenlet* self); +static PyObject* green_getparent(PyGreenlet* self, void* UNUSED(context)); + +static int green_is_gc(PyObject* self); +static PyObject* green_getdead(PyGreenlet* self, void* UNUSED(context)); +static PyObject* green_getrun(PyGreenlet* self, void* UNUSED(context)); +static int green_setcontext(PyGreenlet* self, PyObject* nctx, void* UNUSED(context)); +static PyObject* green_getframe(PyGreenlet* self, void* UNUSED(context)); +static PyObject* green_repr(PyGreenlet* self); +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/PyGreenletUnswitchable.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/PyGreenletUnswitchable.cpp new file mode 100644 index 0000000..1b768ee --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/PyGreenletUnswitchable.cpp @@ -0,0 +1,147 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + Implementation of the Python slots for PyGreenletUnswitchable_Type +*/ +#ifndef PY_GREENLET_UNSWITCHABLE_CPP +#define PY_GREENLET_UNSWITCHABLE_CPP + + + +#define PY_SSIZE_T_CLEAN +#include +#include "structmember.h" // PyMemberDef + +#include "greenlet_internal.hpp" +// Code after this point can assume access to things declared in stdint.h, +// including the fixed-width types. This goes for the platform-specific switch functions +// as well. +#include "greenlet_refs.hpp" +#include "greenlet_slp_switch.hpp" + +#include "greenlet_thread_support.hpp" +#include "TGreenlet.hpp" + +#include "TGreenlet.cpp" +#include "TGreenletGlobals.cpp" +#include "TThreadStateDestroy.cpp" + + +using greenlet::LockGuard; +using greenlet::LockInitError; +using greenlet::PyErrOccurred; +using greenlet::Require; + +using greenlet::g_handle_exit; +using greenlet::single_result; + +using greenlet::Greenlet; +using greenlet::UserGreenlet; +using greenlet::MainGreenlet; +using greenlet::BrokenGreenlet; +using greenlet::ThreadState; +using greenlet::PythonState; + + +#include "PyGreenlet.hpp" + +static PyGreenlet* +green_unswitchable_new(PyTypeObject* type, PyObject* UNUSED(args), PyObject* UNUSED(kwds)) +{ + PyGreenlet* o = + (PyGreenlet*)PyBaseObject_Type.tp_new(type, mod_globs->empty_tuple, mod_globs->empty_dict); + if (o) { + new BrokenGreenlet(o, GET_THREAD_STATE().state().borrow_current()); + assert(Py_REFCNT(o) == 1); + } + return o; +} + +static PyObject* +green_unswitchable_getforce(PyGreenlet* self, void* UNUSED(context)) +{ + BrokenGreenlet* broken = dynamic_cast(self->pimpl); + return PyBool_FromLong(broken->_force_switch_error); +} + +static int +green_unswitchable_setforce(PyGreenlet* self, PyObject* nforce, void* UNUSED(context)) +{ + if (!nforce) { + PyErr_SetString( + PyExc_AttributeError, + "Cannot delete force_switch_error" + ); + return -1; + } + BrokenGreenlet* broken = dynamic_cast(self->pimpl); + int is_true = PyObject_IsTrue(nforce); + if (is_true == -1) { + return -1; + } + broken->_force_switch_error = is_true; + return 0; +} + +static PyObject* +green_unswitchable_getforceslp(PyGreenlet* self, void* UNUSED(context)) +{ + BrokenGreenlet* broken = dynamic_cast(self->pimpl); + return PyBool_FromLong(broken->_force_slp_switch_error); +} + +static int +green_unswitchable_setforceslp(PyGreenlet* self, PyObject* nforce, void* UNUSED(context)) +{ + if (!nforce) { + PyErr_SetString( + PyExc_AttributeError, + "Cannot delete force_slp_switch_error" + ); + return -1; + } + BrokenGreenlet* broken = dynamic_cast(self->pimpl); + int is_true = PyObject_IsTrue(nforce); + if (is_true == -1) { + return -1; + } + broken->_force_slp_switch_error = is_true; + return 0; +} + +static PyGetSetDef green_unswitchable_getsets[] = { + /* name, getter, setter, doc, closure (context pointer) */ + { + .name="force_switch_error", + .get=(getter)green_unswitchable_getforce, + .set=(setter)green_unswitchable_setforce, + .doc=NULL + }, + { + .name="force_slp_switch_error", + .get=(getter)green_unswitchable_getforceslp, + .set=(setter)green_unswitchable_setforceslp, + .doc=nullptr + }, + {.name=nullptr} +}; + +PyTypeObject PyGreenletUnswitchable_Type = { + .ob_base=PyVarObject_HEAD_INIT(NULL, 0) + .tp_name="greenlet._greenlet.UnswitchableGreenlet", + .tp_dealloc= (destructor)green_dealloc, /* tp_dealloc */ + .tp_flags=G_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */ + .tp_doc="Undocumented internal class", /* tp_doc */ + .tp_traverse=(traverseproc)green_traverse, /* tp_traverse */ + .tp_clear=(inquiry)green_clear, /* tp_clear */ + + .tp_getset=green_unswitchable_getsets, /* tp_getset */ + .tp_base=&PyGreenlet_Type, /* tp_base */ + .tp_init=(initproc)green_init, /* tp_init */ + .tp_alloc=PyType_GenericAlloc, /* tp_alloc */ + .tp_new=(newfunc)green_unswitchable_new, /* tp_new */ + .tp_free=PyObject_GC_Del, /* tp_free */ + .tp_is_gc=(inquiry)green_is_gc, /* tp_is_gc */ +}; + + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/PyModule.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/PyModule.cpp new file mode 100644 index 0000000..6adcb5c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/PyModule.cpp @@ -0,0 +1,292 @@ +#ifndef PY_MODULE_CPP +#define PY_MODULE_CPP + +#include "greenlet_internal.hpp" + + +#include "TGreenletGlobals.cpp" +#include "TMainGreenlet.cpp" +#include "TThreadStateDestroy.cpp" + +using greenlet::LockGuard; +using greenlet::ThreadState; + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +# pragma clang diagnostic ignored "-Wunused-variable" +#endif + +PyDoc_STRVAR(mod_getcurrent_doc, + "getcurrent() -> greenlet\n" + "\n" + "Returns the current greenlet (i.e. the one which called this " + "function).\n"); + +static PyObject* +mod_getcurrent(PyObject* UNUSED(module)) +{ + return GET_THREAD_STATE().state().get_current().relinquish_ownership_o(); +} + +PyDoc_STRVAR(mod_settrace_doc, + "settrace(callback) -> object\n" + "\n" + "Sets a new tracing function and returns the previous one.\n"); +static PyObject* +mod_settrace(PyObject* UNUSED(module), PyObject* args) +{ + PyArgParseParam tracefunc; + if (!PyArg_ParseTuple(args, "O", &tracefunc)) { + return NULL; + } + ThreadState& state = GET_THREAD_STATE(); + OwnedObject previous = state.get_tracefunc(); + if (!previous) { + previous = Py_None; + } + + state.set_tracefunc(tracefunc); + + return previous.relinquish_ownership(); +} + +PyDoc_STRVAR(mod_gettrace_doc, + "gettrace() -> object\n" + "\n" + "Returns the currently set tracing function, or None.\n"); + +static PyObject* +mod_gettrace(PyObject* UNUSED(module)) +{ + OwnedObject tracefunc = GET_THREAD_STATE().state().get_tracefunc(); + if (!tracefunc) { + tracefunc = Py_None; + } + return tracefunc.relinquish_ownership(); +} + + + +PyDoc_STRVAR(mod_set_thread_local_doc, + "set_thread_local(key, value) -> None\n" + "\n" + "Set a value in the current thread-local dictionary. Debugging only.\n"); + +static PyObject* +mod_set_thread_local(PyObject* UNUSED(module), PyObject* args) +{ + PyArgParseParam key; + PyArgParseParam value; + PyObject* result = NULL; + + if (PyArg_UnpackTuple(args, "set_thread_local", 2, 2, &key, &value)) { + if(PyDict_SetItem( + PyThreadState_GetDict(), // borrow + key, + value) == 0 ) { + // success + Py_INCREF(Py_None); + result = Py_None; + } + } + return result; +} + +PyDoc_STRVAR(mod_get_pending_cleanup_count_doc, + "get_pending_cleanup_count() -> Integer\n" + "\n" + "Get the number of greenlet cleanup operations pending. Testing only.\n"); + + +static PyObject* +mod_get_pending_cleanup_count(PyObject* UNUSED(module)) +{ + LockGuard cleanup_lock(*mod_globs->thread_states_to_destroy_lock); + return PyLong_FromSize_t(mod_globs->thread_states_to_destroy.size()); +} + +PyDoc_STRVAR(mod_get_total_main_greenlets_doc, + "get_total_main_greenlets() -> Integer\n" + "\n" + "Quickly return the number of main greenlets that exist. Testing only.\n"); + +static PyObject* +mod_get_total_main_greenlets(PyObject* UNUSED(module)) +{ + return PyLong_FromSize_t(G_TOTAL_MAIN_GREENLETS); +} + + + +PyDoc_STRVAR(mod_get_clocks_used_doing_optional_cleanup_doc, + "get_clocks_used_doing_optional_cleanup() -> Integer\n" + "\n" + "Get the number of clock ticks the program has used doing optional " + "greenlet cleanup.\n" + "Beginning in greenlet 2.0, greenlet tries to find and dispose of greenlets\n" + "that leaked after a thread exited. This requires invoking Python's garbage collector,\n" + "which may have a performance cost proportional to the number of live objects.\n" + "This function returns the amount of processor time\n" + "greenlet has used to do this. In programs that run with very large amounts of live\n" + "objects, this metric can be used to decide whether the cost of doing this cleanup\n" + "is worth the memory leak being corrected. If not, you can disable the cleanup\n" + "using ``enable_optional_cleanup(False)``.\n" + "The units are arbitrary and can only be compared to themselves (similarly to ``time.clock()``);\n" + "for example, to see how it scales with your heap. You can attempt to convert them into seconds\n" + "by dividing by the value of CLOCKS_PER_SEC." + "If cleanup has been disabled, returns None." + "\n" + "This is an implementation specific, provisional API. It may be changed or removed\n" + "in the future.\n" + ".. versionadded:: 2.0" + ); +static PyObject* +mod_get_clocks_used_doing_optional_cleanup(PyObject* UNUSED(module)) +{ + std::clock_t& clocks = ThreadState::clocks_used_doing_gc(); + + if (clocks == std::clock_t(-1)) { + Py_RETURN_NONE; + } + // This might not actually work on some implementations; clock_t + // is an opaque type. + return PyLong_FromSsize_t(clocks); +} + +PyDoc_STRVAR(mod_enable_optional_cleanup_doc, + "mod_enable_optional_cleanup(bool) -> None\n" + "\n" + "Enable or disable optional cleanup operations.\n" + "See ``get_clocks_used_doing_optional_cleanup()`` for details.\n" + ); +static PyObject* +mod_enable_optional_cleanup(PyObject* UNUSED(module), PyObject* flag) +{ + int is_true = PyObject_IsTrue(flag); + if (is_true == -1) { + return nullptr; + } + + std::clock_t& clocks = ThreadState::clocks_used_doing_gc(); + if (is_true) { + // If we already have a value, we don't want to lose it. + if (clocks == std::clock_t(-1)) { + clocks = 0; + } + } + else { + clocks = std::clock_t(-1); + } + Py_RETURN_NONE; +} + + + + +#if !GREENLET_PY313 +PyDoc_STRVAR(mod_get_tstate_trash_delete_nesting_doc, + "get_tstate_trash_delete_nesting() -> Integer\n" + "\n" + "Return the 'trash can' nesting level. Testing only.\n"); +static PyObject* +mod_get_tstate_trash_delete_nesting(PyObject* UNUSED(module)) +{ + PyThreadState* tstate = PyThreadState_GET(); + +#if GREENLET_PY312 + return PyLong_FromLong(tstate->trash.delete_nesting); +#else + return PyLong_FromLong(tstate->trash_delete_nesting); +#endif +} +#endif + + + + +static PyMethodDef GreenMethods[] = { + { + .ml_name="getcurrent", + .ml_meth=(PyCFunction)mod_getcurrent, + .ml_flags=METH_NOARGS, + .ml_doc=mod_getcurrent_doc + }, + { + .ml_name="settrace", + .ml_meth=(PyCFunction)mod_settrace, + .ml_flags=METH_VARARGS, + .ml_doc=mod_settrace_doc + }, + { + .ml_name="gettrace", + .ml_meth=(PyCFunction)mod_gettrace, + .ml_flags=METH_NOARGS, + .ml_doc=mod_gettrace_doc + }, + { + .ml_name="set_thread_local", + .ml_meth=(PyCFunction)mod_set_thread_local, + .ml_flags=METH_VARARGS, + .ml_doc=mod_set_thread_local_doc + }, + { + .ml_name="get_pending_cleanup_count", + .ml_meth=(PyCFunction)mod_get_pending_cleanup_count, + .ml_flags=METH_NOARGS, + .ml_doc=mod_get_pending_cleanup_count_doc + }, + { + .ml_name="get_total_main_greenlets", + .ml_meth=(PyCFunction)mod_get_total_main_greenlets, + .ml_flags=METH_NOARGS, + .ml_doc=mod_get_total_main_greenlets_doc + }, + { + .ml_name="get_clocks_used_doing_optional_cleanup", + .ml_meth=(PyCFunction)mod_get_clocks_used_doing_optional_cleanup, + .ml_flags=METH_NOARGS, + .ml_doc=mod_get_clocks_used_doing_optional_cleanup_doc + }, + { + .ml_name="enable_optional_cleanup", + .ml_meth=(PyCFunction)mod_enable_optional_cleanup, + .ml_flags=METH_O, + .ml_doc=mod_enable_optional_cleanup_doc + }, +#if !GREENLET_PY313 + { + .ml_name="get_tstate_trash_delete_nesting", + .ml_meth=(PyCFunction)mod_get_tstate_trash_delete_nesting, + .ml_flags=METH_NOARGS, + .ml_doc=mod_get_tstate_trash_delete_nesting_doc + }, +#endif + {.ml_name=NULL, .ml_meth=NULL} /* Sentinel */ +}; + +static const char* const copy_on_greentype[] = { + "getcurrent", + "error", + "GreenletExit", + "settrace", + "gettrace", + NULL +}; + +static struct PyModuleDef greenlet_module_def = { + .m_base=PyModuleDef_HEAD_INIT, + .m_name="greenlet._greenlet", + .m_doc=NULL, + .m_size=-1, + .m_methods=GreenMethods, +}; + + +#endif + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TBrokenGreenlet.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/TBrokenGreenlet.cpp new file mode 100644 index 0000000..7e9ab5b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TBrokenGreenlet.cpp @@ -0,0 +1,45 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of greenlet::UserGreenlet. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ + +#include "TGreenlet.hpp" + +namespace greenlet { + +void* BrokenGreenlet::operator new(size_t UNUSED(count)) +{ + return allocator.allocate(1); +} + + +void BrokenGreenlet::operator delete(void* ptr) +{ + return allocator.deallocate(static_cast(ptr), + 1); +} + +greenlet::PythonAllocator greenlet::BrokenGreenlet::allocator; + +bool +BrokenGreenlet::force_slp_switch_error() const noexcept +{ + return this->_force_slp_switch_error; +} + +UserGreenlet::switchstack_result_t BrokenGreenlet::g_switchstack(void) +{ + if (this->_force_switch_error) { + return switchstack_result_t(-1); + } + return UserGreenlet::g_switchstack(); +} + +}; //namespace greenlet diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TExceptionState.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/TExceptionState.cpp new file mode 100644 index 0000000..08a94ae --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TExceptionState.cpp @@ -0,0 +1,62 @@ +#ifndef GREENLET_EXCEPTION_STATE_CPP +#define GREENLET_EXCEPTION_STATE_CPP + +#include +#include "TGreenlet.hpp" + +namespace greenlet { + + +ExceptionState::ExceptionState() +{ + this->clear(); +} + +void ExceptionState::operator<<(const PyThreadState *const tstate) noexcept +{ + this->exc_info = tstate->exc_info; + this->exc_state = tstate->exc_state; +} + +void ExceptionState::operator>>(PyThreadState *const tstate) noexcept +{ + tstate->exc_state = this->exc_state; + tstate->exc_info = + this->exc_info ? this->exc_info : &tstate->exc_state; + this->clear(); +} + +void ExceptionState::clear() noexcept +{ + this->exc_info = nullptr; + this->exc_state.exc_value = nullptr; +#if !GREENLET_PY311 + this->exc_state.exc_type = nullptr; + this->exc_state.exc_traceback = nullptr; +#endif + this->exc_state.previous_item = nullptr; +} + +int ExceptionState::tp_traverse(visitproc visit, void* arg) noexcept +{ + Py_VISIT(this->exc_state.exc_value); +#if !GREENLET_PY311 + Py_VISIT(this->exc_state.exc_type); + Py_VISIT(this->exc_state.exc_traceback); +#endif + return 0; +} + +void ExceptionState::tp_clear() noexcept +{ + Py_CLEAR(this->exc_state.exc_value); +#if !GREENLET_PY311 + Py_CLEAR(this->exc_state.exc_type); + Py_CLEAR(this->exc_state.exc_traceback); +#endif +} + + +}; // namespace greenlet + +#endif // GREENLET_EXCEPTION_STATE_CPP diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TGreenlet.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/TGreenlet.cpp new file mode 100644 index 0000000..d12722b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TGreenlet.cpp @@ -0,0 +1,719 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of greenlet::Greenlet. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#ifndef TGREENLET_CPP +#define TGREENLET_CPP +#include "greenlet_internal.hpp" +#include "TGreenlet.hpp" + + +#include "TGreenletGlobals.cpp" +#include "TThreadStateDestroy.cpp" + +namespace greenlet { + +Greenlet::Greenlet(PyGreenlet* p) + : Greenlet(p, StackState()) +{ +} + +Greenlet::Greenlet(PyGreenlet* p, const StackState& initial_stack) + : _self(p), stack_state(initial_stack) +{ + assert(p->pimpl == nullptr); + p->pimpl = this; +} + +Greenlet::~Greenlet() +{ + // XXX: Can't do this. tp_clear is a virtual function, and by the + // time we're here, we've sliced off our child classes. + //this->tp_clear(); + this->_self->pimpl = nullptr; +} + +bool +Greenlet::force_slp_switch_error() const noexcept +{ + return false; +} + +void +Greenlet::release_args() +{ + this->switch_args.CLEAR(); +} + +/** + * CAUTION: This will allocate memory and may trigger garbage + * collection and arbitrary Python code. + */ +OwnedObject +Greenlet::throw_GreenletExit_during_dealloc(const ThreadState& UNUSED(current_thread_state)) +{ + // If we're killed because we lost all references in the + // middle of a switch, that's ok. Don't reset the args/kwargs, + // we still want to pass them to the parent. + PyErr_SetString(mod_globs->PyExc_GreenletExit, + "Killing the greenlet because all references have vanished."); + // To get here it had to have run before + return this->g_switch(); +} + +inline void +Greenlet::slp_restore_state() noexcept +{ +#ifdef SLP_BEFORE_RESTORE_STATE + SLP_BEFORE_RESTORE_STATE(); +#endif + this->stack_state.copy_heap_to_stack( + this->thread_state()->borrow_current()->stack_state); +} + + +inline int +Greenlet::slp_save_state(char *const stackref) noexcept +{ + // XXX: This used to happen in the middle, before saving, but + // after finding the next owner. Does that matter? This is + // only defined for Sparc/GCC where it flushes register + // windows to the stack (I think) +#ifdef SLP_BEFORE_SAVE_STATE + SLP_BEFORE_SAVE_STATE(); +#endif + return this->stack_state.copy_stack_to_heap(stackref, + this->thread_state()->borrow_current()->stack_state); +} + +/** + * CAUTION: This will allocate memory and may trigger garbage + * collection and arbitrary Python code. + */ +OwnedObject +Greenlet::on_switchstack_or_initialstub_failure( + Greenlet* target, + const Greenlet::switchstack_result_t& err, + const bool target_was_me, + const bool was_initial_stub) +{ + // If we get here, either g_initialstub() + // failed, or g_switchstack() failed. Either one of those + // cases SHOULD leave us in the original greenlet with a valid stack. + if (!PyErr_Occurred()) { + PyErr_SetString( + PyExc_SystemError, + was_initial_stub + ? "Failed to switch stacks into a greenlet for the first time." + : "Failed to switch stacks into a running greenlet."); + } + this->release_args(); + + if (target && !target_was_me) { + target->murder_in_place(); + } + + assert(!err.the_new_current_greenlet); + assert(!err.origin_greenlet); + return OwnedObject(); + +} + +OwnedGreenlet +Greenlet::g_switchstack_success() noexcept +{ + PyThreadState* tstate = PyThreadState_GET(); + // restore the saved state + this->python_state >> tstate; + this->exception_state >> tstate; + + // The thread state hasn't been changed yet. + ThreadState* thread_state = this->thread_state(); + OwnedGreenlet result(thread_state->get_current()); + thread_state->set_current(this->self()); + //assert(thread_state->borrow_current().borrow() == this->_self); + return result; +} + +Greenlet::switchstack_result_t +Greenlet::g_switchstack(void) +{ + // if any of these assertions fail, it's likely because we + // switched away and tried to switch back to us. Early stages of + // switching are not reentrant because we re-use ``this->args()``. + // Switching away would happen if we trigger a garbage collection + // (by just using some Python APIs that happen to allocate Python + // objects) and some garbage had weakref callbacks or __del__ that + // switches (people don't write code like that by hand, but with + // gevent it's possible without realizing it) + assert(this->args() || PyErr_Occurred()); + { /* save state */ + if (this->thread_state()->is_current(this->self())) { + // Hmm, nothing to do. + // TODO: Does this bypass trace events that are + // important? + return switchstack_result_t(0, + this, this->thread_state()->borrow_current()); + } + BorrowedGreenlet current = this->thread_state()->borrow_current(); + PyThreadState* tstate = PyThreadState_GET(); + + current->python_state << tstate; + current->exception_state << tstate; + this->python_state.will_switch_from(tstate); + switching_thread_state = this; + current->expose_frames(); + } + assert(this->args() || PyErr_Occurred()); + // If this is the first switch into a greenlet, this will + // return twice, once with 1 in the new greenlet, once with 0 + // in the origin. + int err; + if (this->force_slp_switch_error()) { + err = -1; + } + else { + err = slp_switch(); + } + + if (err < 0) { /* error */ + // Tested by + // test_greenlet.TestBrokenGreenlets.test_failed_to_slp_switch_into_running + // + // It's not clear if it's worth trying to clean up and + // continue here. Failing to switch stacks is a big deal which + // may not be recoverable (who knows what state the stack is in). + // Also, we've stolen references in preparation for calling + // ``g_switchstack_success()`` and we don't have a clean + // mechanism for backing that all out. + Py_FatalError("greenlet: Failed low-level slp_switch(). The stack is probably corrupt."); + } + + // No stack-based variables are valid anymore. + + // But the global is volatile so we can reload it without the + // compiler caching it from earlier. + Greenlet* greenlet_that_switched_in = switching_thread_state; // aka this + switching_thread_state = nullptr; + // except that no stack variables are valid, we would: + // assert(this == greenlet_that_switched_in); + + // switchstack success is where we restore the exception state, + // etc. It returns the origin greenlet because its convenient. + + OwnedGreenlet origin = greenlet_that_switched_in->g_switchstack_success(); + assert(greenlet_that_switched_in->args() || PyErr_Occurred()); + return switchstack_result_t(err, greenlet_that_switched_in, origin); +} + + +inline void +Greenlet::check_switch_allowed() const +{ + // TODO: Make this take a parameter of the current greenlet, + // or current main greenlet, to make the check for + // cross-thread switching cheaper. Surely somewhere up the + // call stack we've already accessed the thread local variable. + + // We expect to always have a main greenlet now; accessing the thread state + // created it. However, if we get here and cleanup has already + // begun because we're a greenlet that was running in a + // (now dead) thread, these invariants will not hold true. In + // fact, accessing `this->thread_state` may not even be possible. + + // If the thread this greenlet was running in is dead, + // we'll still have a reference to a main greenlet, but the + // thread state pointer we have is bogus. + // TODO: Give the objects an API to determine if they belong + // to a dead thread. + + const BorrowedMainGreenlet main_greenlet = this->find_main_greenlet_in_lineage(); + + if (!main_greenlet) { + throw PyErrOccurred(mod_globs->PyExc_GreenletError, + "cannot switch to a garbage collected greenlet"); + } + + if (!main_greenlet->thread_state()) { + throw PyErrOccurred(mod_globs->PyExc_GreenletError, + "cannot switch to a different thread (which happens to have exited)"); + } + + // The main greenlet we found was from the .parent lineage. + // That may or may not have any relationship to the main + // greenlet of the running thread. We can't actually access + // our this->thread_state members to try to check that, + // because it could be in the process of getting destroyed, + // but setting the main_greenlet->thread_state member to NULL + // may not be visible yet. So we need to check against the + // current thread state (once the cheaper checks are out of + // the way) + const BorrowedMainGreenlet current_main_greenlet = GET_THREAD_STATE().state().borrow_main_greenlet(); + if ( + // lineage main greenlet is not this thread's greenlet + current_main_greenlet != main_greenlet + || ( + // atteched to some thread + this->main_greenlet() + // XXX: Same condition as above. Was this supposed to be + // this->main_greenlet()? + && current_main_greenlet != main_greenlet) + // switching into a known dead thread (XXX: which, if we get here, + // is bad, because we just accessed the thread state, which is + // gone!) + || (!current_main_greenlet->thread_state())) { + // CAUTION: This may trigger memory allocations, gc, and + // arbitrary Python code. + throw PyErrOccurred( + mod_globs->PyExc_GreenletError, + "Cannot switch to a different thread\n\tCurrent: %R\n\tExpected: %R", + current_main_greenlet, main_greenlet); + } +} + +const OwnedObject +Greenlet::context() const +{ + using greenlet::PythonStateContext; + OwnedObject result; + + if (this->is_currently_running_in_some_thread()) { + /* Currently running greenlet: context is stored in the thread state, + not the greenlet object. */ + if (GET_THREAD_STATE().state().is_current(this->self())) { + result = PythonStateContext::context(PyThreadState_GET()); + } + else { + throw ValueError( + "cannot get context of a " + "greenlet that is running in a different thread"); + } + } + else { + /* Greenlet is not running: just return context. */ + result = this->python_state.context(); + } + if (!result) { + result = OwnedObject::None(); + } + return result; +} + + +void +Greenlet::context(BorrowedObject given) +{ + using greenlet::PythonStateContext; + if (!given) { + throw AttributeError("can't delete context attribute"); + } + if (given.is_None()) { + /* "Empty context" is stored as NULL, not None. */ + given = nullptr; + } + + //checks type, incrs refcnt + greenlet::refs::OwnedContext context(given); + PyThreadState* tstate = PyThreadState_GET(); + + if (this->is_currently_running_in_some_thread()) { + if (!GET_THREAD_STATE().state().is_current(this->self())) { + throw ValueError("cannot set context of a greenlet" + " that is running in a different thread"); + } + + /* Currently running greenlet: context is stored in the thread state, + not the greenlet object. */ + OwnedObject octx = OwnedObject::consuming(PythonStateContext::context(tstate)); + PythonStateContext::context(tstate, context.relinquish_ownership()); + } + else { + /* Greenlet is not running: just set context. Note that the + greenlet may be dead.*/ + this->python_state.context() = context; + } +} + +/** + * CAUTION: May invoke arbitrary Python code. + * + * Figure out what the result of ``greenlet.switch(arg, kwargs)`` + * should be and transfers ownership of it to the left-hand-side. + * + * If switch() was just passed an arg tuple, then we'll just return that. + * If only keyword arguments were passed, then we'll pass the keyword + * argument dict. Otherwise, we'll create a tuple of (args, kwargs) and + * return both. + * + * CAUTION: This may allocate a new tuple object, which may + * cause the Python garbage collector to run, which in turn may + * run arbitrary Python code that switches. + */ +OwnedObject& operator<<=(OwnedObject& lhs, greenlet::SwitchingArgs& rhs) noexcept +{ + // Because this may invoke arbitrary Python code, which could + // result in switching back to us, we need to get the + // arguments locally on the stack. + assert(rhs); + OwnedObject args = rhs.args(); + OwnedObject kwargs = rhs.kwargs(); + rhs.CLEAR(); + // We shouldn't be called twice for the same switch. + assert(args || kwargs); + assert(!rhs); + + if (!kwargs) { + lhs = args; + } + else if (!PyDict_Size(kwargs.borrow())) { + lhs = args; + } + else if (!PySequence_Length(args.borrow())) { + lhs = kwargs; + } + else { + // PyTuple_Pack allocates memory, may GC, may run arbitrary + // Python code. + lhs = OwnedObject::consuming(PyTuple_Pack(2, args.borrow(), kwargs.borrow())); + } + return lhs; +} + +static OwnedObject +g_handle_exit(const OwnedObject& greenlet_result) +{ + if (!greenlet_result && mod_globs->PyExc_GreenletExit.PyExceptionMatches()) { + /* catch and ignore GreenletExit */ + PyErrFetchParam val; + PyErr_Fetch(PyErrFetchParam(), val, PyErrFetchParam()); + if (!val) { + return OwnedObject::None(); + } + return OwnedObject(val); + } + + if (greenlet_result) { + // package the result into a 1-tuple + // PyTuple_Pack increments the reference of its arguments, + // so we always need to decref the greenlet result; + // the owner will do that. + return OwnedObject::consuming(PyTuple_Pack(1, greenlet_result.borrow())); + } + + return OwnedObject(); +} + + + +/** + * May run arbitrary Python code. + */ +OwnedObject +Greenlet::g_switch_finish(const switchstack_result_t& err) +{ + assert(err.the_new_current_greenlet == this); + + ThreadState& state = *this->thread_state(); + // Because calling the trace function could do arbitrary things, + // including switching away from this greenlet and then maybe + // switching back, we need to capture the arguments now so that + // they don't change. + OwnedObject result; + if (this->args()) { + result <<= this->args(); + } + else { + assert(PyErr_Occurred()); + } + assert(!this->args()); + try { + // Our only caller handles the bad error case + assert(err.status >= 0); + assert(state.borrow_current() == this->self()); + if (OwnedObject tracefunc = state.get_tracefunc()) { + assert(result || PyErr_Occurred()); + g_calltrace(tracefunc, + result ? mod_globs->event_switch : mod_globs->event_throw, + err.origin_greenlet, + this->self()); + } + // The above could have invoked arbitrary Python code, but + // it couldn't switch back to this object and *also* + // throw an exception, so the args won't have changed. + + if (PyErr_Occurred()) { + // We get here if we fell of the end of the run() function + // raising an exception. The switch itself was + // successful, but the function raised. + // valgrind reports that memory allocated here can still + // be reached after a test run. + throw PyErrOccurred::from_current(); + } + return result; + } + catch (const PyErrOccurred&) { + /* Turn switch errors into switch throws */ + /* Turn trace errors into switch throws */ + this->release_args(); + throw; + } +} + +void +Greenlet::g_calltrace(const OwnedObject& tracefunc, + const greenlet::refs::ImmortalEventName& event, + const BorrowedGreenlet& origin, + const BorrowedGreenlet& target) +{ + PyErrPieces saved_exc; + try { + TracingGuard tracing_guard; + // TODO: We have saved the active exception (if any) that's + // about to be raised. In the 'throw' case, we could provide + // the exception to the tracefunction, which seems very helpful. + tracing_guard.CallTraceFunction(tracefunc, event, origin, target); + } + catch (const PyErrOccurred&) { + // In case of exceptions trace function is removed, + // and any existing exception is replaced with the tracing + // exception. + GET_THREAD_STATE().state().set_tracefunc(Py_None); + throw; + } + + saved_exc.PyErrRestore(); + assert( + (event == mod_globs->event_throw && PyErr_Occurred()) + || (event == mod_globs->event_switch && !PyErr_Occurred()) + ); +} + +void +Greenlet::murder_in_place() +{ + if (this->active()) { + assert(!this->is_currently_running_in_some_thread()); + this->deactivate_and_free(); + } +} + +inline void +Greenlet::deactivate_and_free() +{ + if (!this->active()) { + return; + } + // Throw away any saved stack. + this->stack_state = StackState(); + assert(!this->stack_state.active()); + // Throw away any Python references. + // We're holding a borrowed reference to the last + // frame we executed. Since we borrowed it, the + // normal traversal, clear, and dealloc functions + // ignore it, meaning it leaks. (The thread state + // object can't find it to clear it when that's + // deallocated either, because by definition if we + // got an object on this list, it wasn't + // running and the thread state doesn't have + // this frame.) + // So here, we *do* clear it. + this->python_state.tp_clear(true); +} + +bool +Greenlet::belongs_to_thread(const ThreadState* thread_state) const +{ + if (!this->thread_state() // not running anywhere, or thread + // exited + || !thread_state) { // same, or there is no thread state. + return false; + } + return true; +} + + +void +Greenlet::deallocing_greenlet_in_thread(const ThreadState* current_thread_state) +{ + /* Cannot raise an exception to kill the greenlet if + it is not running in the same thread! */ + if (this->belongs_to_thread(current_thread_state)) { + assert(current_thread_state); + // To get here it had to have run before + /* Send the greenlet a GreenletExit exception. */ + + // We don't care about the return value, only whether an + // exception happened. + this->throw_GreenletExit_during_dealloc(*current_thread_state); + return; + } + + // Not the same thread! Temporarily save the greenlet + // into its thread's deleteme list, *if* it exists. + // If that thread has already exited, and processed its pending + // cleanup, we'll never be able to clean everything up: we won't + // be able to raise an exception. + // That's mostly OK! Since we can't add it to a list, our refcount + // won't increase, and we'll go ahead with the DECREFs later. + + ThreadState *const thread_state = this->thread_state(); + if (thread_state) { + thread_state->delete_when_thread_running(this->self()); + } + else { + // The thread is dead, we can't raise an exception. + // We need to make it look non-active, though, so that dealloc + // finishes killing it. + this->deactivate_and_free(); + } + return; +} + + +int +Greenlet::tp_traverse(visitproc visit, void* arg) +{ + + int result; + if ((result = this->exception_state.tp_traverse(visit, arg)) != 0) { + return result; + } + //XXX: This is ugly. But so is handling everything having to do + //with the top frame. + bool visit_top_frame = this->was_running_in_dead_thread(); + // When true, the thread is dead. Our implicit weak reference to the + // frame is now all that's left; we consider ourselves to + // strongly own it now. + if ((result = this->python_state.tp_traverse(visit, arg, visit_top_frame)) != 0) { + return result; + } + return 0; +} + +int +Greenlet::tp_clear() +{ + bool own_top_frame = this->was_running_in_dead_thread(); + this->exception_state.tp_clear(); + this->python_state.tp_clear(own_top_frame); + return 0; +} + +bool Greenlet::is_currently_running_in_some_thread() const +{ + return this->stack_state.active() && !this->python_state.top_frame(); +} + +#if GREENLET_PY312 +void GREENLET_NOINLINE(Greenlet::expose_frames)() +{ + if (!this->python_state.top_frame()) { + return; + } + + _PyInterpreterFrame* last_complete_iframe = nullptr; + _PyInterpreterFrame* iframe = this->python_state.top_frame()->f_frame; + while (iframe) { + // We must make a copy before looking at the iframe contents, + // since iframe might point to a portion of the greenlet's C stack + // that was spilled when switching greenlets. + _PyInterpreterFrame iframe_copy; + this->stack_state.copy_from_stack(&iframe_copy, iframe, sizeof(*iframe)); + if (!_PyFrame_IsIncomplete(&iframe_copy)) { + // If the iframe were OWNED_BY_CSTACK then it would always be + // incomplete. Since it's not incomplete, it's not on the C stack + // and we can access it through the original `iframe` pointer + // directly. This is important since GetFrameObject might + // lazily _create_ the frame object and we don't want the + // interpreter to lose track of it. + assert(iframe_copy.owner != FRAME_OWNED_BY_CSTACK); + + // We really want to just write: + // PyFrameObject* frame = _PyFrame_GetFrameObject(iframe); + // but _PyFrame_GetFrameObject calls _PyFrame_MakeAndSetFrameObject + // which is not a visible symbol in libpython. The easiest + // way to get a public function to call it is using + // PyFrame_GetBack, which is defined as follows: + // assert(frame != NULL); + // assert(!_PyFrame_IsIncomplete(frame->f_frame)); + // PyFrameObject *back = frame->f_back; + // if (back == NULL) { + // _PyInterpreterFrame *prev = frame->f_frame->previous; + // prev = _PyFrame_GetFirstComplete(prev); + // if (prev) { + // back = _PyFrame_GetFrameObject(prev); + // } + // } + // return (PyFrameObject*)Py_XNewRef(back); + if (!iframe->frame_obj) { + PyFrameObject dummy_frame; + _PyInterpreterFrame dummy_iframe; + dummy_frame.f_back = nullptr; + dummy_frame.f_frame = &dummy_iframe; + // force the iframe to be considered complete without + // needing to check its code object: + dummy_iframe.owner = FRAME_OWNED_BY_GENERATOR; + dummy_iframe.previous = iframe; + assert(!_PyFrame_IsIncomplete(&dummy_iframe)); + // Drop the returned reference immediately; the iframe + // continues to hold a strong reference + Py_XDECREF(PyFrame_GetBack(&dummy_frame)); + assert(iframe->frame_obj); + } + + // This is a complete frame, so make the last one of those we saw + // point at it, bypassing any incomplete frames (which may have + // been on the C stack) in between the two. We're overwriting + // last_complete_iframe->previous and need that to be reversible, + // so we store the original previous ptr in the frame object + // (which we must have created on a previous iteration through + // this loop). The frame object has a bunch of storage that is + // only used when its iframe is OWNED_BY_FRAME_OBJECT, which only + // occurs when the frame object outlives the frame's execution, + // which can't have happened yet because the frame is currently + // executing as far as the interpreter is concerned. So, we can + // reuse it for our own purposes. + assert(iframe->owner == FRAME_OWNED_BY_THREAD + || iframe->owner == FRAME_OWNED_BY_GENERATOR); + if (last_complete_iframe) { + assert(last_complete_iframe->frame_obj); + memcpy(&last_complete_iframe->frame_obj->_f_frame_data[0], + &last_complete_iframe->previous, sizeof(void *)); + last_complete_iframe->previous = iframe; + } + last_complete_iframe = iframe; + } + // Frames that are OWNED_BY_FRAME_OBJECT are linked via the + // frame's f_back while all others are linked via the iframe's + // previous ptr. Since all the frames we traverse are running + // as far as the interpreter is concerned, we don't have to + // worry about the OWNED_BY_FRAME_OBJECT case. + iframe = iframe_copy.previous; + } + + // Give the outermost complete iframe a null previous pointer to + // account for any potential incomplete/C-stack iframes between it + // and the actual top-of-stack + if (last_complete_iframe) { + assert(last_complete_iframe->frame_obj); + memcpy(&last_complete_iframe->frame_obj->_f_frame_data[0], + &last_complete_iframe->previous, sizeof(void *)); + last_complete_iframe->previous = nullptr; + } +} +#else +void Greenlet::expose_frames() +{ + +} +#endif + +}; // namespace greenlet +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TGreenlet.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/TGreenlet.hpp new file mode 100644 index 0000000..e152353 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TGreenlet.hpp @@ -0,0 +1,830 @@ +#ifndef GREENLET_GREENLET_HPP +#define GREENLET_GREENLET_HPP +/* + * Declarations of the core data structures. +*/ + +#define PY_SSIZE_T_CLEAN +#include + +#include "greenlet_compiler_compat.hpp" +#include "greenlet_refs.hpp" +#include "greenlet_cpython_compat.hpp" +#include "greenlet_allocator.hpp" + +using greenlet::refs::OwnedObject; +using greenlet::refs::OwnedGreenlet; +using greenlet::refs::OwnedMainGreenlet; +using greenlet::refs::BorrowedGreenlet; + +#if PY_VERSION_HEX < 0x30B00A6 +# define _PyCFrame CFrame +# define _PyInterpreterFrame _interpreter_frame +#endif + +#if GREENLET_PY312 +# define Py_BUILD_CORE +# include "internal/pycore_frame.h" +#endif + +#if GREENLET_PY314 +# include "internal/pycore_interpframe_structs.h" +#if defined(_MSC_VER) || defined(__MINGW64__) +# include "greenlet_msvc_compat.hpp" +#else +# include "internal/pycore_interpframe.h" +#endif +#endif + +// XXX: TODO: Work to remove all virtual functions +// for speed of calling and size of objects (no vtable). +// One pattern is the Curiously Recurring Template +namespace greenlet +{ + class ExceptionState + { + private: + G_NO_COPIES_OF_CLS(ExceptionState); + + // Even though these are borrowed objects, we actually own + // them, when they're not null. + // XXX: Express that in the API. + private: + _PyErr_StackItem* exc_info; + _PyErr_StackItem exc_state; + public: + ExceptionState(); + void operator<<(const PyThreadState *const tstate) noexcept; + void operator>>(PyThreadState* tstate) noexcept; + void clear() noexcept; + + int tp_traverse(visitproc visit, void* arg) noexcept; + void tp_clear() noexcept; + }; + + template + void operator<<(const PyThreadState *const tstate, T& exc); + + class PythonStateContext + { + protected: + greenlet::refs::OwnedContext _context; + public: + inline const greenlet::refs::OwnedContext& context() const + { + return this->_context; + } + inline greenlet::refs::OwnedContext& context() + { + return this->_context; + } + + inline void tp_clear() + { + this->_context.CLEAR(); + } + + template + inline static PyObject* context(T* tstate) + { + return tstate->context; + } + + template + inline static void context(T* tstate, PyObject* new_context) + { + tstate->context = new_context; + tstate->context_ver++; + } + }; + class SwitchingArgs; + class PythonState : public PythonStateContext + { + public: + typedef greenlet::refs::OwnedReference OwnedFrame; + private: + G_NO_COPIES_OF_CLS(PythonState); + // We own this if we're suspended (although currently we don't + // tp_traverse into it; that's a TODO). If we're running, it's + // empty. If we get deallocated and *still* have a frame, it + // won't be reachable from the place that normally decref's + // it, so we need to do it (hence owning it). + OwnedFrame _top_frame; +#if GREENLET_USE_CFRAME + _PyCFrame* cframe; + int use_tracing; +#endif +#if GREENLET_PY314 + int py_recursion_depth; + // I think this is only used by the JIT. At least, + // we only got errors not switching it when the JIT was enabled. + // Python/generated_cases.c.h:12469: _PyEval_EvalFrameDefault: + // Assertion `tstate->current_executor == NULL' failed. + // see https://github.com/python-greenlet/greenlet/issues/460 + PyObject* current_executor; +#elif GREENLET_PY312 + int py_recursion_depth; + int c_recursion_depth; +#else + int recursion_depth; +#endif +#if GREENLET_PY313 + PyObject *delete_later; +#else + int trash_delete_nesting; +#endif +#if GREENLET_PY311 + _PyInterpreterFrame* current_frame; + _PyStackChunk* datastack_chunk; + PyObject** datastack_top; + PyObject** datastack_limit; +#endif + // The PyInterpreterFrame list on 3.12+ contains some entries that are + // on the C stack, which can't be directly accessed while a greenlet is + // suspended. In order to keep greenlet gr_frame introspection working, + // we adjust stack switching to rewrite the interpreter frame list + // to skip these C-stack frames; we call this "exposing" the greenlet's + // frames because it makes them valid to work with in Python. Then when + // the greenlet is resumed we need to remember to reverse the operation + // we did. The C-stack frames are "entry frames" which are a low-level + // interpreter detail; they're not needed for introspection, but do + // need to be present for the eval loop to work. + void unexpose_frames(); + + public: + + PythonState(); + // You can use this for testing whether we have a frame + // or not. It returns const so they can't modify it. + const OwnedFrame& top_frame() const noexcept; + + inline void operator<<(const PyThreadState *const tstate) noexcept; + inline void operator>>(PyThreadState* tstate) noexcept; + void clear() noexcept; + + int tp_traverse(visitproc visit, void* arg, bool visit_top_frame) noexcept; + void tp_clear(bool own_top_frame) noexcept; + void set_initial_state(const PyThreadState* const tstate) noexcept; +#if GREENLET_USE_CFRAME + void set_new_cframe(_PyCFrame& frame) noexcept; +#endif + + void may_switch_away() noexcept; + inline void will_switch_from(PyThreadState *const origin_tstate) noexcept; + void did_finish(PyThreadState* tstate) noexcept; + }; + + class StackState + { + // By having only plain C (POD) members, no virtual functions + // or bases, we get a trivial assignment operator generated + // for us. However, that's not safe since we do manage memory. + // So we declare an assignment operator that only works if we + // don't have any memory allocated. (We don't use + // std::shared_ptr for reference counting just to keep this + // object small) + private: + char* _stack_start; + char* stack_stop; + char* stack_copy; + intptr_t _stack_saved; + StackState* stack_prev; + inline int copy_stack_to_heap_up_to(const char* const stop) noexcept; + inline void free_stack_copy() noexcept; + + public: + /** + * Creates a started, but inactive, state, using *current* + * as the previous. + */ + StackState(void* mark, StackState& current); + /** + * Creates an inactive, unstarted, state. + */ + StackState(); + ~StackState(); + StackState(const StackState& other); + StackState& operator=(const StackState& other); + inline void copy_heap_to_stack(const StackState& current) noexcept; + inline int copy_stack_to_heap(char* const stackref, const StackState& current) noexcept; + inline bool started() const noexcept; + inline bool main() const noexcept; + inline bool active() const noexcept; + inline void set_active() noexcept; + inline void set_inactive() noexcept; + inline intptr_t stack_saved() const noexcept; + inline char* stack_start() const noexcept; + static inline StackState make_main() noexcept; +#ifdef GREENLET_USE_STDIO + friend std::ostream& operator<<(std::ostream& os, const StackState& s); +#endif + + // Fill in [dest, dest + n) with the values that would be at + // [src, src + n) while this greenlet is running. This is like memcpy + // except that if the greenlet is suspended it accounts for the portion + // of the greenlet's stack that was spilled to the heap. `src` may + // be on this greenlet's stack, or on the heap, but not on a different + // greenlet's stack. + void copy_from_stack(void* dest, const void* src, size_t n) const; + }; +#ifdef GREENLET_USE_STDIO + std::ostream& operator<<(std::ostream& os, const StackState& s); +#endif + + class SwitchingArgs + { + private: + G_NO_ASSIGNMENT_OF_CLS(SwitchingArgs); + // If args and kwargs are both false (NULL), this is a *throw*, not a + // switch. PyErr_... must have been called already. + OwnedObject _args; + OwnedObject _kwargs; + public: + + SwitchingArgs() + {} + + SwitchingArgs(const OwnedObject& args, const OwnedObject& kwargs) + : _args(args), + _kwargs(kwargs) + {} + + SwitchingArgs(const SwitchingArgs& other) + : _args(other._args), + _kwargs(other._kwargs) + {} + + const OwnedObject& args() + { + return this->_args; + } + + const OwnedObject& kwargs() + { + return this->_kwargs; + } + + /** + * Moves ownership from the argument to this object. + */ + SwitchingArgs& operator<<=(SwitchingArgs& other) + { + if (this != &other) { + this->_args = other._args; + this->_kwargs = other._kwargs; + other.CLEAR(); + } + return *this; + } + + /** + * Acquires ownership of the argument (consumes the reference). + */ + SwitchingArgs& operator<<=(PyObject* args) + { + this->_args = OwnedObject::consuming(args); + this->_kwargs.CLEAR(); + return *this; + } + + /** + * Acquires ownership of the argument. + * + * Sets the args to be the given value; clears the kwargs. + */ + SwitchingArgs& operator<<=(OwnedObject& args) + { + assert(&args != &this->_args); + this->_args = args; + this->_kwargs.CLEAR(); + args.CLEAR(); + + return *this; + } + + explicit operator bool() const noexcept + { + return this->_args || this->_kwargs; + } + + inline void CLEAR() + { + this->_args.CLEAR(); + this->_kwargs.CLEAR(); + } + + const std::string as_str() const noexcept + { + return PyUnicode_AsUTF8( + OwnedObject::consuming( + PyUnicode_FromFormat( + "SwitchingArgs(args=%R, kwargs=%R)", + this->_args.borrow(), + this->_kwargs.borrow() + ) + ).borrow() + ); + } + }; + + class ThreadState; + + class UserGreenlet; + class MainGreenlet; + + class Greenlet + { + private: + G_NO_COPIES_OF_CLS(Greenlet); + PyGreenlet* const _self; + private: + // XXX: Work to remove these. + friend class ThreadState; + friend class UserGreenlet; + friend class MainGreenlet; + protected: + ExceptionState exception_state; + SwitchingArgs switch_args; + StackState stack_state; + PythonState python_state; + Greenlet(PyGreenlet* p, const StackState& initial_state); + public: + // This constructor takes ownership of the PyGreenlet, by + // setting ``p->pimpl = this;``. + Greenlet(PyGreenlet* p); + virtual ~Greenlet(); + + const OwnedObject context() const; + + // You MUST call this _very_ early in the switching process to + // prepare anything that may need prepared. This might perform + // garbage collections or otherwise run arbitrary Python code. + // + // One specific use of it is for Python 3.11+, preventing + // running arbitrary code at unsafe times. See + // PythonState::may_switch_away(). + inline void may_switch_away() + { + this->python_state.may_switch_away(); + } + + inline void context(refs::BorrowedObject new_context); + + inline SwitchingArgs& args() + { + return this->switch_args; + } + + virtual const refs::BorrowedMainGreenlet main_greenlet() const = 0; + + inline intptr_t stack_saved() const noexcept + { + return this->stack_state.stack_saved(); + } + + // This is used by the macro SLP_SAVE_STATE to compute the + // difference in stack sizes. It might be nice to handle the + // computation ourself, but the type of the result + // varies by platform, so doing it in the macro is the + // simplest way. + inline const char* stack_start() const noexcept + { + return this->stack_state.stack_start(); + } + + virtual OwnedObject throw_GreenletExit_during_dealloc(const ThreadState& current_thread_state); + virtual OwnedObject g_switch() = 0; + /** + * Force the greenlet to appear dead. Used when it's not + * possible to throw an exception into a greenlet anymore. + * + * This losses access to the thread state and the main greenlet. + */ + virtual void murder_in_place(); + + /** + * Called when somebody notices we were running in a dead + * thread to allow cleaning up resources (because we can't + * raise GreenletExit into it anymore). + * This is very similar to ``murder_in_place()``, except that + * it DOES NOT lose the main greenlet or thread state. + */ + inline void deactivate_and_free(); + + + // Called when some thread wants to deallocate a greenlet + // object. + // The thread may or may not be the same thread the greenlet + // was running in. + // The thread state will be null if the thread the greenlet + // was running in was known to have exited. + void deallocing_greenlet_in_thread(const ThreadState* current_state); + + // Must be called on 3.12+ before exposing a suspended greenlet's + // frames to user code. This rewrites the linked list of interpreter + // frames to skip the ones that are being stored on the C stack (which + // can't be safely accessed while the greenlet is suspended because + // that stack space might be hosting a different greenlet), and + // sets PythonState::frames_were_exposed so we remember to restore + // the original list before resuming the greenlet. The C-stack frames + // are a low-level interpreter implementation detail; while they're + // important to the bytecode eval loop, they're superfluous for + // introspection purposes. + void expose_frames(); + + + // TODO: Figure out how to make these non-public. + inline void slp_restore_state() noexcept; + inline int slp_save_state(char *const stackref) noexcept; + + inline bool is_currently_running_in_some_thread() const; + virtual bool belongs_to_thread(const ThreadState* state) const; + + inline bool started() const + { + return this->stack_state.started(); + } + inline bool active() const + { + return this->stack_state.active(); + } + inline bool main() const + { + return this->stack_state.main(); + } + virtual refs::BorrowedMainGreenlet find_main_greenlet_in_lineage() const = 0; + + virtual const OwnedGreenlet parent() const = 0; + virtual void parent(const refs::BorrowedObject new_parent) = 0; + + inline const PythonState::OwnedFrame& top_frame() + { + return this->python_state.top_frame(); + } + + virtual const OwnedObject& run() const = 0; + virtual void run(const refs::BorrowedObject nrun) = 0; + + + virtual int tp_traverse(visitproc visit, void* arg); + virtual int tp_clear(); + + + // Return the thread state that the greenlet is running in, or + // null if the greenlet is not running or the thread is known + // to have exited. + virtual ThreadState* thread_state() const noexcept = 0; + + // Return true if the greenlet is known to have been running + // (active) in a thread that has now exited. + virtual bool was_running_in_dead_thread() const noexcept = 0; + + // Return a borrowed greenlet that is the Python object + // this object represents. + inline BorrowedGreenlet self() const noexcept + { + return BorrowedGreenlet(this->_self); + } + + // For testing. If this returns true, we should pretend that + // slp_switch() failed. + virtual bool force_slp_switch_error() const noexcept; + + protected: + inline void release_args(); + + // The functions that must not be inlined are declared virtual. + // We also mark them as protected, not private, so that the + // compiler is forced to call them through a function pointer. + // (A sufficiently smart compiler could directly call a private + // virtual function since it can never be overridden in a + // subclass). + + // Also TODO: Switch away from integer error codes and to enums, + // or throw exceptions when possible. + struct switchstack_result_t + { + int status; + Greenlet* the_new_current_greenlet; + OwnedGreenlet origin_greenlet; + + switchstack_result_t() + : status(0), + the_new_current_greenlet(nullptr) + {} + + switchstack_result_t(int err) + : status(err), + the_new_current_greenlet(nullptr) + {} + + switchstack_result_t(int err, Greenlet* state, OwnedGreenlet& origin) + : status(err), + the_new_current_greenlet(state), + origin_greenlet(origin) + { + } + + switchstack_result_t(int err, Greenlet* state, const BorrowedGreenlet& origin) + : status(err), + the_new_current_greenlet(state), + origin_greenlet(origin) + { + } + + switchstack_result_t(const switchstack_result_t& other) + : status(other.status), + the_new_current_greenlet(other.the_new_current_greenlet), + origin_greenlet(other.origin_greenlet) + {} + + switchstack_result_t& operator=(const switchstack_result_t& other) + { + this->status = other.status; + this->the_new_current_greenlet = other.the_new_current_greenlet; + this->origin_greenlet = other.origin_greenlet; + return *this; + } + }; + + OwnedObject on_switchstack_or_initialstub_failure( + Greenlet* target, + const switchstack_result_t& err, + const bool target_was_me=false, + const bool was_initial_stub=false); + + // Returns the previous greenlet we just switched away from. + virtual OwnedGreenlet g_switchstack_success() noexcept; + + + // Check the preconditions for switching to this greenlet; if they + // aren't met, throws PyErrOccurred. Most callers will want to + // catch this and clear the arguments + inline void check_switch_allowed() const; + class GreenletStartedWhileInPython : public std::runtime_error + { + public: + GreenletStartedWhileInPython() : std::runtime_error("") + {} + }; + + protected: + + + /** + Perform a stack switch into this greenlet. + + This temporarily sets the global variable + ``switching_thread_state`` to this greenlet; as soon as the + call to ``slp_switch`` completes, this is reset to NULL. + Consequently, this depends on the GIL. + + TODO: Adopt the stackman model and pass ``slp_switch`` a + callback function and context pointer; this eliminates the + need for global variables altogether. + + Because the stack switch happens in this function, this + function can't use its own stack (local) variables, set + before the switch, and then accessed after the switch. + + Further, you con't even access ``g_thread_state_global`` + before and after the switch from the global variable. + Because it is thread local some compilers cache it in a + register/on the stack, notably new versions of MSVC; this + breaks with strange crashes sometime later, because writing + to anything in ``g_thread_state_global`` after the switch + is actually writing to random memory. For this reason, we + call a non-inlined function to finish the operation. (XXX: + The ``/GT`` MSVC compiler argument probably fixes that.) + + It is very important that stack switch is 'atomic', i.e. no + calls into other Python code allowed (except very few that + are safe), because global variables are very fragile. (This + should no longer be the case with thread-local variables.) + + */ + // Made virtual to facilitate subclassing UserGreenlet for testing. + virtual switchstack_result_t g_switchstack(void); + +class TracingGuard +{ +private: + PyThreadState* tstate; +public: + TracingGuard() + : tstate(PyThreadState_GET()) + { + PyThreadState_EnterTracing(this->tstate); + } + + ~TracingGuard() + { + PyThreadState_LeaveTracing(this->tstate); + this->tstate = nullptr; + } + + inline void CallTraceFunction(const OwnedObject& tracefunc, + const greenlet::refs::ImmortalEventName& event, + const BorrowedGreenlet& origin, + const BorrowedGreenlet& target) + { + // TODO: This calls tracefunc(event, (origin, target)). Add a shortcut + // function for that that's specialized to avoid the Py_BuildValue + // string parsing, or start with just using "ON" format with PyTuple_Pack(2, + // origin, target). That seems like what the N format is meant + // for. + // XXX: Why does event not automatically cast back to a PyObject? + // It tries to call the "deleted constructor ImmortalEventName + // const" instead. + assert(tracefunc); + assert(event); + assert(origin); + assert(target); + greenlet::refs::NewReference retval( + PyObject_CallFunction( + tracefunc.borrow(), + "O(OO)", + event.borrow(), + origin.borrow(), + target.borrow() + )); + if (!retval) { + throw PyErrOccurred::from_current(); + } + } +}; + + static void + g_calltrace(const OwnedObject& tracefunc, + const greenlet::refs::ImmortalEventName& event, + const greenlet::refs::BorrowedGreenlet& origin, + const BorrowedGreenlet& target); + private: + OwnedObject g_switch_finish(const switchstack_result_t& err); + + }; + + class UserGreenlet : public Greenlet + { + private: + static greenlet::PythonAllocator allocator; + OwnedMainGreenlet _main_greenlet; + OwnedObject _run_callable; + OwnedGreenlet _parent; + public: + static void* operator new(size_t UNUSED(count)); + static void operator delete(void* ptr); + + UserGreenlet(PyGreenlet* p, BorrowedGreenlet the_parent); + virtual ~UserGreenlet(); + + virtual refs::BorrowedMainGreenlet find_main_greenlet_in_lineage() const; + virtual bool was_running_in_dead_thread() const noexcept; + virtual ThreadState* thread_state() const noexcept; + virtual OwnedObject g_switch(); + virtual const OwnedObject& run() const + { + if (this->started() || !this->_run_callable) { + throw AttributeError("run"); + } + return this->_run_callable; + } + virtual void run(const refs::BorrowedObject nrun); + + virtual const OwnedGreenlet parent() const; + virtual void parent(const refs::BorrowedObject new_parent); + + virtual const refs::BorrowedMainGreenlet main_greenlet() const; + + virtual void murder_in_place(); + virtual bool belongs_to_thread(const ThreadState* state) const; + virtual int tp_traverse(visitproc visit, void* arg); + virtual int tp_clear(); + class ParentIsCurrentGuard + { + private: + OwnedGreenlet oldparent; + UserGreenlet* greenlet; + G_NO_COPIES_OF_CLS(ParentIsCurrentGuard); + public: + ParentIsCurrentGuard(UserGreenlet* p, const ThreadState& thread_state); + ~ParentIsCurrentGuard(); + }; + virtual OwnedObject throw_GreenletExit_during_dealloc(const ThreadState& current_thread_state); + protected: + virtual switchstack_result_t g_initialstub(void* mark); + private: + // This function isn't meant to return. + // This accepts raw pointers and the ownership of them at the + // same time. The caller should use ``inner_bootstrap(origin.relinquish_ownership())``. + void inner_bootstrap(PyGreenlet* origin_greenlet, PyObject* run); + }; + + class BrokenGreenlet : public UserGreenlet + { + private: + static greenlet::PythonAllocator allocator; + public: + bool _force_switch_error = false; + bool _force_slp_switch_error = false; + + static void* operator new(size_t UNUSED(count)); + static void operator delete(void* ptr); + BrokenGreenlet(PyGreenlet* p, BorrowedGreenlet the_parent) + : UserGreenlet(p, the_parent) + {} + virtual ~BrokenGreenlet() + {} + + virtual switchstack_result_t g_switchstack(void); + virtual bool force_slp_switch_error() const noexcept; + + }; + + class MainGreenlet : public Greenlet + { + private: + static greenlet::PythonAllocator allocator; + refs::BorrowedMainGreenlet _self; + ThreadState* _thread_state; + G_NO_COPIES_OF_CLS(MainGreenlet); + public: + static void* operator new(size_t UNUSED(count)); + static void operator delete(void* ptr); + + MainGreenlet(refs::BorrowedMainGreenlet::PyType*, ThreadState*); + virtual ~MainGreenlet(); + + + virtual const OwnedObject& run() const; + virtual void run(const refs::BorrowedObject nrun); + + virtual const OwnedGreenlet parent() const; + virtual void parent(const refs::BorrowedObject new_parent); + + virtual const refs::BorrowedMainGreenlet main_greenlet() const; + + virtual refs::BorrowedMainGreenlet find_main_greenlet_in_lineage() const; + virtual bool was_running_in_dead_thread() const noexcept; + virtual ThreadState* thread_state() const noexcept; + void thread_state(ThreadState*) noexcept; + virtual OwnedObject g_switch(); + virtual int tp_traverse(visitproc visit, void* arg); + }; + + // Instantiate one on the stack to save the GC state, + // and then disable GC. When it goes out of scope, GC will be + // restored to its original state. Sadly, these APIs are only + // available on 3.10+; luckily, we only need them on 3.11+. +#if GREENLET_PY310 + class GCDisabledGuard + { + private: + int was_enabled = 0; + public: + GCDisabledGuard() + : was_enabled(PyGC_IsEnabled()) + { + PyGC_Disable(); + } + + ~GCDisabledGuard() + { + if (this->was_enabled) { + PyGC_Enable(); + } + } + }; +#endif + + OwnedObject& operator<<=(OwnedObject& lhs, greenlet::SwitchingArgs& rhs) noexcept; + + //TODO: Greenlet::g_switch() should call this automatically on its + //return value. As it is, the module code is calling it. + static inline OwnedObject + single_result(const OwnedObject& results) + { + if (results + && PyTuple_Check(results.borrow()) + && PyTuple_GET_SIZE(results.borrow()) == 1) { + PyObject* result = PyTuple_GET_ITEM(results.borrow(), 0); + assert(result); + return OwnedObject::owning(result); + } + return results; + } + + + static OwnedObject + g_handle_exit(const OwnedObject& greenlet_result); + + + template + void operator<<(const PyThreadState *const lhs, T& rhs) + { + rhs.operator<<(lhs); + } + +} // namespace greenlet ; + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TGreenletGlobals.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/TGreenletGlobals.cpp new file mode 100644 index 0000000..0087d2f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TGreenletGlobals.cpp @@ -0,0 +1,94 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of GreenletGlobals. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#ifndef T_GREENLET_GLOBALS +#define T_GREENLET_GLOBALS + +#include "greenlet_refs.hpp" +#include "greenlet_exceptions.hpp" +#include "greenlet_thread_support.hpp" +#include "greenlet_internal.hpp" + +namespace greenlet { + +// This encapsulates what were previously module global "constants" +// established at init time. +// This is a step towards Python3 style module state that allows +// reloading. +// +// In an earlier iteration of this code, we used placement new to be +// able to allocate this object statically still, so that references +// to its members don't incur an extra pointer indirection. +// But under some scenarios, that could result in crashes at +// shutdown because apparently the destructor was getting run twice? +class GreenletGlobals +{ + +public: + const greenlet::refs::ImmortalEventName event_switch; + const greenlet::refs::ImmortalEventName event_throw; + const greenlet::refs::ImmortalException PyExc_GreenletError; + const greenlet::refs::ImmortalException PyExc_GreenletExit; + const greenlet::refs::ImmortalObject empty_tuple; + const greenlet::refs::ImmortalObject empty_dict; + const greenlet::refs::ImmortalString str_run; + Mutex* const thread_states_to_destroy_lock; + greenlet::cleanup_queue_t thread_states_to_destroy; + + GreenletGlobals() : + event_switch("switch"), + event_throw("throw"), + PyExc_GreenletError("greenlet.error"), + PyExc_GreenletExit("greenlet.GreenletExit", PyExc_BaseException), + empty_tuple(Require(PyTuple_New(0))), + empty_dict(Require(PyDict_New())), + str_run("run"), + thread_states_to_destroy_lock(new Mutex()) + {} + + ~GreenletGlobals() + { + // This object is (currently) effectively immortal, and not + // just because of those placement new tricks; if we try to + // deallocate the static object we allocated, and overwrote, + // we would be doing so at C++ teardown time, which is after + // the final Python GIL is released, and we can't use the API + // then. + // (The members will still be destructed, but they also don't + // do any deallocation.) + } + + void queue_to_destroy(ThreadState* ts) const + { + // we're currently accessed through a static const object, + // implicitly marking our members as const, so code can't just + // call push_back (or pop_back) without casting away the + // const. + // + // Do that for callers. + greenlet::cleanup_queue_t& q = const_cast(this->thread_states_to_destroy); + q.push_back(ts); + } + + ThreadState* take_next_to_destroy() const + { + greenlet::cleanup_queue_t& q = const_cast(this->thread_states_to_destroy); + ThreadState* result = q.back(); + q.pop_back(); + return result; + } +}; + +}; // namespace greenlet + +static const greenlet::GreenletGlobals* mod_globs; + +#endif // T_GREENLET_GLOBALS diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TMainGreenlet.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/TMainGreenlet.cpp new file mode 100644 index 0000000..a2a9cfe --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TMainGreenlet.cpp @@ -0,0 +1,153 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of greenlet::MainGreenlet. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#ifndef T_MAIN_GREENLET_CPP +#define T_MAIN_GREENLET_CPP + +#include "TGreenlet.hpp" + + + +// Protected by the GIL. Incremented when we create a main greenlet, +// in a new thread, decremented when it is destroyed. +static Py_ssize_t G_TOTAL_MAIN_GREENLETS; + +namespace greenlet { +greenlet::PythonAllocator MainGreenlet::allocator; + +void* MainGreenlet::operator new(size_t UNUSED(count)) +{ + return allocator.allocate(1); +} + + +void MainGreenlet::operator delete(void* ptr) +{ + return allocator.deallocate(static_cast(ptr), + 1); +} + + +MainGreenlet::MainGreenlet(PyGreenlet* p, ThreadState* state) + : Greenlet(p, StackState::make_main()), + _self(p), + _thread_state(state) +{ + G_TOTAL_MAIN_GREENLETS++; +} + +MainGreenlet::~MainGreenlet() +{ + G_TOTAL_MAIN_GREENLETS--; + this->tp_clear(); +} + +ThreadState* +MainGreenlet::thread_state() const noexcept +{ + return this->_thread_state; +} + +void +MainGreenlet::thread_state(ThreadState* t) noexcept +{ + assert(!t); + this->_thread_state = t; +} + + +const BorrowedMainGreenlet +MainGreenlet::main_greenlet() const +{ + return this->_self; +} + +BorrowedMainGreenlet +MainGreenlet::find_main_greenlet_in_lineage() const +{ + return BorrowedMainGreenlet(this->_self); +} + +bool +MainGreenlet::was_running_in_dead_thread() const noexcept +{ + return !this->_thread_state; +} + +OwnedObject +MainGreenlet::g_switch() +{ + try { + this->check_switch_allowed(); + } + catch (const PyErrOccurred&) { + this->release_args(); + throw; + } + + switchstack_result_t err = this->g_switchstack(); + if (err.status < 0) { + // XXX: This code path is untested, but it is shared + // with the UserGreenlet path that is tested. + return this->on_switchstack_or_initialstub_failure( + this, + err, + true, // target was me + false // was initial stub + ); + } + + return err.the_new_current_greenlet->g_switch_finish(err); +} + +int +MainGreenlet::tp_traverse(visitproc visit, void* arg) +{ + if (this->_thread_state) { + // we've already traversed main, (self), don't do it again. + int result = this->_thread_state->tp_traverse(visit, arg, false); + if (result) { + return result; + } + } + return Greenlet::tp_traverse(visit, arg); +} + +const OwnedObject& +MainGreenlet::run() const +{ + throw AttributeError("Main greenlets do not have a run attribute."); +} + +void +MainGreenlet::run(const BorrowedObject UNUSED(nrun)) +{ + throw AttributeError("Main greenlets do not have a run attribute."); +} + +void +MainGreenlet::parent(const BorrowedObject raw_new_parent) +{ + if (!raw_new_parent) { + throw AttributeError("can't delete attribute"); + } + throw AttributeError("cannot set the parent of a main greenlet"); +} + +const OwnedGreenlet +MainGreenlet::parent() const +{ + return OwnedGreenlet(); // null becomes None +} + +}; // namespace greenlet + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TPythonState.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/TPythonState.cpp new file mode 100644 index 0000000..8833a80 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TPythonState.cpp @@ -0,0 +1,406 @@ +#ifndef GREENLET_PYTHON_STATE_CPP +#define GREENLET_PYTHON_STATE_CPP + +#include +#include "TGreenlet.hpp" + +namespace greenlet { + +PythonState::PythonState() + : _top_frame() +#if GREENLET_USE_CFRAME + ,cframe(nullptr) + ,use_tracing(0) +#endif +#if GREENLET_PY314 + ,py_recursion_depth(0) + ,current_executor(nullptr) +#elif GREENLET_PY312 + ,py_recursion_depth(0) + ,c_recursion_depth(0) +#else + ,recursion_depth(0) +#endif +#if GREENLET_PY313 + ,delete_later(nullptr) +#else + ,trash_delete_nesting(0) +#endif +#if GREENLET_PY311 + ,current_frame(nullptr) + ,datastack_chunk(nullptr) + ,datastack_top(nullptr) + ,datastack_limit(nullptr) +#endif +{ +#if GREENLET_USE_CFRAME + /* + The PyThreadState->cframe pointer usually points to memory on + the stack, alloceted in a call into PyEval_EvalFrameDefault. + + Initially, before any evaluation begins, it points to the + initial PyThreadState object's ``root_cframe`` object, which is + statically allocated for the lifetime of the thread. + + A greenlet can last for longer than a call to + PyEval_EvalFrameDefault, so we can't set its ``cframe`` pointer + to be the current ``PyThreadState->cframe``; nor could we use + one from the greenlet parent for the same reason. Yet a further + no: we can't allocate one scoped to the greenlet and then + destroy it when the greenlet is deallocated, because inside the + interpreter the _PyCFrame objects form a linked list, and that too + can result in accessing memory beyond its dynamic lifetime (if + the greenlet doesn't actually finish before it dies, its entry + could still be in the list). + + Using the ``root_cframe`` is problematic, though, because its + members are never modified by the interpreter and are set to 0, + meaning that its ``use_tracing`` flag is never updated. We don't + want to modify that value in the ``root_cframe`` ourself: it + *shouldn't* matter much because we should probably never get + back to the point where that's the only cframe on the stack; + even if it did matter, the major consequence of an incorrect + value for ``use_tracing`` is that if its true the interpreter + does some extra work --- however, it's just good code hygiene. + + Our solution: before a greenlet runs, after its initial + creation, it uses the ``root_cframe`` just to have something to + put there. However, once the greenlet is actually switched to + for the first time, ``g_initialstub`` (which doesn't actually + "return" while the greenlet is running) stores a new _PyCFrame on + its local stack, and copies the appropriate values from the + currently running _PyCFrame; this is then made the _PyCFrame for the + newly-minted greenlet. ``g_initialstub`` then proceeds to call + ``glet.run()``, which results in ``PyEval_...`` adding the + _PyCFrame to the list. Switches continue as normal. Finally, when + the greenlet finishes, the call to ``glet.run()`` returns and + the _PyCFrame is taken out of the linked list and the stack value + is now unused and free to expire. + + XXX: I think we can do better. If we're deallocing in the same + thread, can't we traverse the list and unlink our frame? + Can we just keep a reference to the thread state in case we + dealloc in another thread? (Is that even possible if we're still + running and haven't returned from g_initialstub?) + */ + this->cframe = &PyThreadState_GET()->root_cframe; +#endif +} + + +inline void PythonState::may_switch_away() noexcept +{ +#if GREENLET_PY311 + // PyThreadState_GetFrame is probably going to have to allocate a + // new frame object. That may trigger garbage collection. Because + // we call this during the early phases of a switch (it doesn't + // matter to which greenlet, as this has a global effect), if a GC + // triggers a switch away, two things can happen, both bad: + // - We might not get switched back to, halting forward progress. + // this is pathological, but possible. + // - We might get switched back to with a different set of + // arguments or a throw instead of a switch. That would corrupt + // our state (specifically, PyErr_Occurred() and this->args() + // would no longer agree). + // + // Thus, when we call this API, we need to have GC disabled. + // This method serves as a bottleneck we call when maybe beginning + // a switch. In this way, it is always safe -- no risk of GC -- to + // use ``_GetFrame()`` whenever we need to, just as it was in + // <=3.10 (because subsequent calls will be cached and not + // allocate memory). + + GCDisabledGuard no_gc; + Py_XDECREF(PyThreadState_GetFrame(PyThreadState_GET())); +#endif +} + +void PythonState::operator<<(const PyThreadState *const tstate) noexcept +{ + this->_context.steal(tstate->context); +#if GREENLET_USE_CFRAME + /* + IMPORTANT: ``cframe`` is a pointer into the STACK. Thus, because + the call to ``slp_switch()`` changes the contents of the stack, + you cannot read from ``ts_current->cframe`` after that call and + necessarily get the same values you get from reading it here. + Anything you need to restore from now to then must be saved in a + global/threadlocal variable (because we can't use stack + variables here either). For things that need to persist across + the switch, use `will_switch_from`. + */ + this->cframe = tstate->cframe; + #if !GREENLET_PY312 + this->use_tracing = tstate->cframe->use_tracing; + #endif +#endif // GREENLET_USE_CFRAME +#if GREENLET_PY311 + #if GREENLET_PY314 + this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; + this->current_executor = tstate->current_executor; + #elif GREENLET_PY312 + this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; + this->c_recursion_depth = Py_C_RECURSION_LIMIT - tstate->c_recursion_remaining; + #else // not 312 + this->recursion_depth = tstate->recursion_limit - tstate->recursion_remaining; + #endif // GREENLET_PY312 + #if GREENLET_PY313 + this->current_frame = tstate->current_frame; + #elif GREENLET_USE_CFRAME + this->current_frame = tstate->cframe->current_frame; + #endif + this->datastack_chunk = tstate->datastack_chunk; + this->datastack_top = tstate->datastack_top; + this->datastack_limit = tstate->datastack_limit; + + PyFrameObject *frame = PyThreadState_GetFrame((PyThreadState *)tstate); + Py_XDECREF(frame); // PyThreadState_GetFrame gives us a new + // reference. + this->_top_frame.steal(frame); + #if GREENLET_PY313 + this->delete_later = Py_XNewRef(tstate->delete_later); + #elif GREENLET_PY312 + this->trash_delete_nesting = tstate->trash.delete_nesting; + #else // not 312 + this->trash_delete_nesting = tstate->trash_delete_nesting; + #endif // GREENLET_PY312 +#else // Not 311 + this->recursion_depth = tstate->recursion_depth; + this->_top_frame.steal(tstate->frame); + this->trash_delete_nesting = tstate->trash_delete_nesting; +#endif // GREENLET_PY311 +} + +#if GREENLET_PY312 +void GREENLET_NOINLINE(PythonState::unexpose_frames)() +{ + if (!this->top_frame()) { + return; + } + + // See GreenletState::expose_frames() and the comment on frames_were_exposed + // for more information about this logic. + _PyInterpreterFrame *iframe = this->_top_frame->f_frame; + while (iframe != nullptr) { + _PyInterpreterFrame *prev_exposed = iframe->previous; + assert(iframe->frame_obj); + memcpy(&iframe->previous, &iframe->frame_obj->_f_frame_data[0], + sizeof(void *)); + iframe = prev_exposed; + } +} +#else +void PythonState::unexpose_frames() +{} +#endif + +void PythonState::operator>>(PyThreadState *const tstate) noexcept +{ + tstate->context = this->_context.relinquish_ownership(); + /* Incrementing this value invalidates the contextvars cache, + which would otherwise remain valid across switches */ + tstate->context_ver++; +#if GREENLET_USE_CFRAME + tstate->cframe = this->cframe; + /* + If we were tracing, we need to keep tracing. + There should never be the possibility of hitting the + root_cframe here. See note above about why we can't + just copy this from ``origin->cframe->use_tracing``. + */ + #if !GREENLET_PY312 + tstate->cframe->use_tracing = this->use_tracing; + #endif +#endif // GREENLET_USE_CFRAME +#if GREENLET_PY311 + #if GREENLET_PY314 + tstate->py_recursion_remaining = tstate->py_recursion_limit - this->py_recursion_depth; + tstate->current_executor = this->current_executor; + this->unexpose_frames(); + #elif GREENLET_PY312 + tstate->py_recursion_remaining = tstate->py_recursion_limit - this->py_recursion_depth; + tstate->c_recursion_remaining = Py_C_RECURSION_LIMIT - this->c_recursion_depth; + this->unexpose_frames(); + #else // \/ 3.11 + tstate->recursion_remaining = tstate->recursion_limit - this->recursion_depth; + #endif // GREENLET_PY312 + #if GREENLET_PY313 + tstate->current_frame = this->current_frame; + #elif GREENLET_USE_CFRAME + tstate->cframe->current_frame = this->current_frame; + #endif + tstate->datastack_chunk = this->datastack_chunk; + tstate->datastack_top = this->datastack_top; + tstate->datastack_limit = this->datastack_limit; + this->_top_frame.relinquish_ownership(); + #if GREENLET_PY313 + Py_XDECREF(tstate->delete_later); + tstate->delete_later = this->delete_later; + Py_CLEAR(this->delete_later); + #elif GREENLET_PY312 + tstate->trash.delete_nesting = this->trash_delete_nesting; + #else // not 3.12 + tstate->trash_delete_nesting = this->trash_delete_nesting; + #endif // GREENLET_PY312 +#else // not 3.11 + tstate->frame = this->_top_frame.relinquish_ownership(); + tstate->recursion_depth = this->recursion_depth; + tstate->trash_delete_nesting = this->trash_delete_nesting; +#endif // GREENLET_PY311 +} + +inline void PythonState::will_switch_from(PyThreadState *const origin_tstate) noexcept +{ +#if GREENLET_USE_CFRAME && !GREENLET_PY312 + // The weird thing is, we don't actually save this for an + // effect on the current greenlet, it's saved for an + // effect on the target greenlet. That is, we want + // continuity of this setting across the greenlet switch. + this->use_tracing = origin_tstate->cframe->use_tracing; +#endif +} + +void PythonState::set_initial_state(const PyThreadState* const tstate) noexcept +{ + this->_top_frame = nullptr; +#if GREENLET_PY314 + this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; + this->current_executor = tstate->current_executor; +#elif GREENLET_PY312 + this->py_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; + // XXX: TODO: Comment from a reviewer: + // Should this be ``Py_C_RECURSION_LIMIT - tstate->c_recursion_remaining``? + // But to me it looks more like that might not be the right + // initialization either? + this->c_recursion_depth = tstate->py_recursion_limit - tstate->py_recursion_remaining; +#elif GREENLET_PY311 + this->recursion_depth = tstate->recursion_limit - tstate->recursion_remaining; +#else + this->recursion_depth = tstate->recursion_depth; +#endif +} +// TODO: Better state management about when we own the top frame. +int PythonState::tp_traverse(visitproc visit, void* arg, bool own_top_frame) noexcept +{ + Py_VISIT(this->_context.borrow()); + if (own_top_frame) { + Py_VISIT(this->_top_frame.borrow()); + } + return 0; +} + +void PythonState::tp_clear(bool own_top_frame) noexcept +{ + PythonStateContext::tp_clear(); + // If we get here owning a frame, + // we got dealloc'd without being finished. We may or may not be + // in the same thread. + if (own_top_frame) { + this->_top_frame.CLEAR(); + } +} + +#if GREENLET_USE_CFRAME +void PythonState::set_new_cframe(_PyCFrame& frame) noexcept +{ + frame = *PyThreadState_GET()->cframe; + /* Make the target greenlet refer to the stack value. */ + this->cframe = &frame; + /* + And restore the link to the previous frame so this one gets + unliked appropriately. + */ + this->cframe->previous = &PyThreadState_GET()->root_cframe; +} +#endif + +const PythonState::OwnedFrame& PythonState::top_frame() const noexcept +{ + return this->_top_frame; +} + +void PythonState::did_finish(PyThreadState* tstate) noexcept +{ +#if GREENLET_PY311 + // See https://github.com/gevent/gevent/issues/1924 and + // https://github.com/python-greenlet/greenlet/issues/328. In + // short, Python 3.11 allocates memory for frames as a sort of + // linked list that's kept as part of PyThreadState in the + // ``datastack_chunk`` member and friends. These are saved and + // restored as part of switching greenlets. + // + // When we initially switch to a greenlet, we set those to NULL. + // That causes the frame management code to treat this like a + // brand new thread and start a fresh list of chunks, beginning + // with a new "root" chunk. As we make calls in this greenlet, + // those chunks get added, and as calls return, they get popped. + // But the frame code (pystate.c) is careful to make sure that the + // root chunk never gets popped. + // + // Thus, when a greenlet exits for the last time, there will be at + // least a single root chunk that we must be responsible for + // deallocating. + // + // The complex part is that these chunks are allocated and freed + // using ``_PyObject_VirtualAlloc``/``Free``. Those aren't public + // functions, and they aren't exported for linking. It so happens + // that we know they are just thin wrappers around the Arena + // allocator, so we can use that directly to deallocate in a + // compatible way. + // + // CAUTION: Check this implementation detail on every major version. + // + // It might be nice to be able to do this in our destructor, but + // can we be sure that no one else is using that memory? Plus, as + // described below, our pointers may not even be valid anymore. As + // a special case, there is one time that we know we can do this, + // and that's from the destructor of the associated UserGreenlet + // (NOT main greenlet) + PyObjectArenaAllocator alloc; + _PyStackChunk* chunk = nullptr; + if (tstate) { + // We really did finish, we can never be switched to again. + chunk = tstate->datastack_chunk; + // Unfortunately, we can't do much sanity checking. Our + // this->datastack_chunk pointer is out of date (evaluation may + // have popped down through it already) so we can't verify that + // we deallocate it. I don't think we can even check datastack_top + // for the same reason. + + PyObject_GetArenaAllocator(&alloc); + tstate->datastack_chunk = nullptr; + tstate->datastack_limit = nullptr; + tstate->datastack_top = nullptr; + + } + else if (this->datastack_chunk) { + // The UserGreenlet (NOT the main greenlet!) is being deallocated. If we're + // still holding a stack chunk, it's garbage because we know + // we can never switch back to let cPython clean it up. + // Because the last time we got switched away from, and we + // haven't run since then, we know our chain is valid and can + // be dealloced. + chunk = this->datastack_chunk; + PyObject_GetArenaAllocator(&alloc); + } + + if (alloc.free && chunk) { + // In case the arena mechanism has been torn down already. + while (chunk) { + _PyStackChunk *prev = chunk->previous; + chunk->previous = nullptr; + alloc.free(alloc.ctx, chunk, chunk->size); + chunk = prev; + } + } + + this->datastack_chunk = nullptr; + this->datastack_limit = nullptr; + this->datastack_top = nullptr; +#endif +} + + +}; // namespace greenlet + +#endif // GREENLET_PYTHON_STATE_CPP diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TStackState.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/TStackState.cpp new file mode 100644 index 0000000..9743ab5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TStackState.cpp @@ -0,0 +1,265 @@ +#ifndef GREENLET_STACK_STATE_CPP +#define GREENLET_STACK_STATE_CPP + +#include "TGreenlet.hpp" + +namespace greenlet { + +#ifdef GREENLET_USE_STDIO +#include +using std::cerr; +using std::endl; + +std::ostream& operator<<(std::ostream& os, const StackState& s) +{ + os << "StackState(stack_start=" << (void*)s._stack_start + << ", stack_stop=" << (void*)s.stack_stop + << ", stack_copy=" << (void*)s.stack_copy + << ", stack_saved=" << s._stack_saved + << ", stack_prev=" << s.stack_prev + << ", addr=" << &s + << ")"; + return os; +} +#endif + +StackState::StackState(void* mark, StackState& current) + : _stack_start(nullptr), + stack_stop((char*)mark), + stack_copy(nullptr), + _stack_saved(0), + /* Skip a dying greenlet */ + stack_prev(current._stack_start + ? ¤t + : current.stack_prev) +{ +} + +StackState::StackState() + : _stack_start(nullptr), + stack_stop(nullptr), + stack_copy(nullptr), + _stack_saved(0), + stack_prev(nullptr) +{ +} + +StackState::StackState(const StackState& other) +// can't use a delegating constructor because of +// MSVC for Python 2.7 + : _stack_start(nullptr), + stack_stop(nullptr), + stack_copy(nullptr), + _stack_saved(0), + stack_prev(nullptr) +{ + this->operator=(other); +} + +StackState& StackState::operator=(const StackState& other) +{ + if (&other == this) { + return *this; + } + if (other._stack_saved) { + throw std::runtime_error("Refusing to steal memory."); + } + + //If we have memory allocated, dispose of it + this->free_stack_copy(); + + this->_stack_start = other._stack_start; + this->stack_stop = other.stack_stop; + this->stack_copy = other.stack_copy; + this->_stack_saved = other._stack_saved; + this->stack_prev = other.stack_prev; + return *this; +} + +inline void StackState::free_stack_copy() noexcept +{ + PyMem_Free(this->stack_copy); + this->stack_copy = nullptr; + this->_stack_saved = 0; +} + +inline void StackState::copy_heap_to_stack(const StackState& current) noexcept +{ + + /* Restore the heap copy back into the C stack */ + if (this->_stack_saved != 0) { + memcpy(this->_stack_start, this->stack_copy, this->_stack_saved); + this->free_stack_copy(); + } + StackState* owner = const_cast(¤t); + if (!owner->_stack_start) { + owner = owner->stack_prev; /* greenlet is dying, skip it */ + } + while (owner && owner->stack_stop <= this->stack_stop) { + // cerr << "\tOwner: " << owner << endl; + owner = owner->stack_prev; /* find greenlet with more stack */ + } + this->stack_prev = owner; + // cerr << "\tFinished with: " << *this << endl; +} + +inline int StackState::copy_stack_to_heap_up_to(const char* const stop) noexcept +{ + /* Save more of g's stack into the heap -- at least up to 'stop' + g->stack_stop |________| + | | + | __ stop . . . . . + | | ==> . . + |________| _______ + | | | | + | | | | + g->stack_start | | |_______| g->stack_copy + */ + intptr_t sz1 = this->_stack_saved; + intptr_t sz2 = stop - this->_stack_start; + assert(this->_stack_start); + if (sz2 > sz1) { + char* c = (char*)PyMem_Realloc(this->stack_copy, sz2); + if (!c) { + PyErr_NoMemory(); + return -1; + } + memcpy(c + sz1, this->_stack_start + sz1, sz2 - sz1); + this->stack_copy = c; + this->_stack_saved = sz2; + } + return 0; +} + +inline int StackState::copy_stack_to_heap(char* const stackref, + const StackState& current) noexcept +{ + /* must free all the C stack up to target_stop */ + const char* const target_stop = this->stack_stop; + + StackState* owner = const_cast(¤t); + assert(owner->_stack_saved == 0); // everything is present on the stack + if (!owner->_stack_start) { + owner = owner->stack_prev; /* not saved if dying */ + } + else { + owner->_stack_start = stackref; + } + + while (owner->stack_stop < target_stop) { + /* ts_current is entierely within the area to free */ + if (owner->copy_stack_to_heap_up_to(owner->stack_stop)) { + return -1; /* XXX */ + } + owner = owner->stack_prev; + } + if (owner != this) { + if (owner->copy_stack_to_heap_up_to(target_stop)) { + return -1; /* XXX */ + } + } + return 0; +} + +inline bool StackState::started() const noexcept +{ + return this->stack_stop != nullptr; +} + +inline bool StackState::main() const noexcept +{ + return this->stack_stop == (char*)-1; +} + +inline bool StackState::active() const noexcept +{ + return this->_stack_start != nullptr; +} + +inline void StackState::set_active() noexcept +{ + assert(this->_stack_start == nullptr); + this->_stack_start = (char*)1; +} + +inline void StackState::set_inactive() noexcept +{ + this->_stack_start = nullptr; + // XXX: What if we still have memory out there? + // That case is actually triggered by + // test_issue251_issue252_explicit_reference_not_collectable (greenlet.tests.test_leaks.TestLeaks) + // and + // test_issue251_issue252_need_to_collect_in_background + // (greenlet.tests.test_leaks.TestLeaks) + // + // Those objects never get deallocated, so the destructor never + // runs. + // It *seems* safe to clean up the memory here? + if (this->_stack_saved) { + this->free_stack_copy(); + } +} + +inline intptr_t StackState::stack_saved() const noexcept +{ + return this->_stack_saved; +} + +inline char* StackState::stack_start() const noexcept +{ + return this->_stack_start; +} + + +inline StackState StackState::make_main() noexcept +{ + StackState s; + s._stack_start = (char*)1; + s.stack_stop = (char*)-1; + return s; +} + +StackState::~StackState() +{ + if (this->_stack_saved != 0) { + this->free_stack_copy(); + } +} + +void StackState::copy_from_stack(void* vdest, const void* vsrc, size_t n) const +{ + char* dest = static_cast(vdest); + const char* src = static_cast(vsrc); + if (src + n <= this->_stack_start + || src >= this->_stack_start + this->_stack_saved + || this->_stack_saved == 0) { + // Nothing we're copying was spilled from the stack + memcpy(dest, src, n); + return; + } + + if (src < this->_stack_start) { + // Copy the part before the saved stack. + // We know src + n > _stack_start due to the test above. + const size_t nbefore = this->_stack_start - src; + memcpy(dest, src, nbefore); + dest += nbefore; + src += nbefore; + n -= nbefore; + } + // We know src >= _stack_start after the before-copy, and + // src < _stack_start + _stack_saved due to the first if condition + size_t nspilled = std::min(n, this->_stack_start + this->_stack_saved - src); + memcpy(dest, this->stack_copy + (src - this->_stack_start), nspilled); + dest += nspilled; + src += nspilled; + n -= nspilled; + if (n > 0) { + // Copy the part after the saved stack + memcpy(dest, src, n); + } +} + +}; // namespace greenlet + +#endif // GREENLET_STACK_STATE_CPP diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TThreadState.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/TThreadState.hpp new file mode 100644 index 0000000..e4e6f6c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TThreadState.hpp @@ -0,0 +1,497 @@ +#ifndef GREENLET_THREAD_STATE_HPP +#define GREENLET_THREAD_STATE_HPP + +#include +#include + +#include "greenlet_internal.hpp" +#include "greenlet_refs.hpp" +#include "greenlet_thread_support.hpp" + +using greenlet::refs::BorrowedObject; +using greenlet::refs::BorrowedGreenlet; +using greenlet::refs::BorrowedMainGreenlet; +using greenlet::refs::OwnedMainGreenlet; +using greenlet::refs::OwnedObject; +using greenlet::refs::OwnedGreenlet; +using greenlet::refs::OwnedList; +using greenlet::refs::PyErrFetchParam; +using greenlet::refs::PyArgParseParam; +using greenlet::refs::ImmortalString; +using greenlet::refs::CreatedModule; +using greenlet::refs::PyErrPieces; +using greenlet::refs::NewReference; + +namespace greenlet { +/** + * Thread-local state of greenlets. + * + * Each native thread will get exactly one of these objects, + * automatically accessed through the best available thread-local + * mechanism the compiler supports (``thread_local`` for C++11 + * compilers or ``__thread``/``declspec(thread)`` for older GCC/clang + * or MSVC, respectively.) + * + * Previously, we kept thread-local state mostly in a bunch of + * ``static volatile`` variables in the main greenlet file.. This had + * the problem of requiring extra checks, loops, and great care + * accessing these variables if we potentially invoked any Python code + * that could release the GIL, because the state could change out from + * under us. Making the variables thread-local solves this problem. + * + * When we detected that a greenlet API accessing the current greenlet + * was invoked from a different thread than the greenlet belonged to, + * we stored a reference to the greenlet in the Python thread + * dictionary for the thread the greenlet belonged to. This could lead + * to memory leaks if the thread then exited (because of a reference + * cycle, as greenlets referred to the thread dictionary, and deleting + * non-current greenlets leaked their frame plus perhaps arguments on + * the C stack). If a thread exited while still having running + * greenlet objects (perhaps that had just switched back to the main + * greenlet), and did not invoke one of the greenlet APIs *in that + * thread, immediately before it exited, without some other thread + * then being invoked*, such a leak was guaranteed. + * + * This can be partly solved by using compiler thread-local variables + * instead of the Python thread dictionary, thus avoiding a cycle. + * + * To fully solve this problem, we need a reliable way to know that a + * thread is done and we should clean up the main greenlet. On POSIX, + * we can use the destructor function of ``pthread_key_create``, but + * there's nothing similar on Windows; a C++11 thread local object + * reliably invokes its destructor when the thread it belongs to exits + * (non-C++11 compilers offer ``__thread`` or ``declspec(thread)`` to + * create thread-local variables, but they can't hold C++ objects that + * invoke destructors; the C++11 version is the most portable solution + * I found). When the thread exits, we can drop references and + * otherwise manipulate greenlets and frames that we know can no + * longer be switched to. For compilers that don't support C++11 + * thread locals, we have a solution that uses the python thread + * dictionary, though it may not collect everything as promptly as + * other compilers do, if some other library is using the thread + * dictionary and has a cycle or extra reference. + * + * There are two small wrinkles. The first is that when the thread + * exits, it is too late to actually invoke Python APIs: the Python + * thread state is gone, and the GIL is released. To solve *this* + * problem, our destructor uses ``Py_AddPendingCall`` to transfer the + * destruction work to the main thread. (This is not an issue for the + * dictionary solution.) + * + * The second is that once the thread exits, the thread local object + * is invalid and we can't even access a pointer to it, so we can't + * pass it to ``Py_AddPendingCall``. This is handled by actually using + * a second object that's thread local (ThreadStateCreator) and having + * it dynamically allocate this object so it can live until the + * pending call runs. + */ + + + +class ThreadState { +private: + // As of commit 08ad1dd7012b101db953f492e0021fb08634afad + // this class needed 56 bytes in o Py_DEBUG build + // on 64-bit macOS 11. + // Adding the vector takes us up to 80 bytes () + + /* Strong reference to the main greenlet */ + OwnedMainGreenlet main_greenlet; + + /* Strong reference to the current greenlet. */ + OwnedGreenlet current_greenlet; + + /* Strong reference to the trace function, if any. */ + OwnedObject tracefunc; + + typedef std::vector > deleteme_t; + /* A vector of raw PyGreenlet pointers representing things that need + deleted when this thread is running. The vector owns the + references, but you need to manually INCREF/DECREF as you use + them. We don't use a vector because we + make copy of this vector, and that would become O(n) as all the + refcounts are incremented in the copy. + */ + deleteme_t deleteme; + +#ifdef GREENLET_NEEDS_EXCEPTION_STATE_SAVED + void* exception_state; +#endif + + static std::clock_t _clocks_used_doing_gc; + static ImmortalString get_referrers_name; + static PythonAllocator allocator; + + G_NO_COPIES_OF_CLS(ThreadState); + + + // Allocates a main greenlet for the thread state. If this fails, + // exits the process. Called only during constructing a ThreadState. + MainGreenlet* alloc_main() + { + PyGreenlet* gmain; + + /* create the main greenlet for this thread */ + gmain = reinterpret_cast(PyType_GenericAlloc(&PyGreenlet_Type, 0)); + if (gmain == NULL) { + throw PyFatalError("alloc_main failed to alloc"); //exits the process + } + + MainGreenlet* const main = new MainGreenlet(gmain, this); + + assert(Py_REFCNT(gmain) == 1); + assert(gmain->pimpl == main); + return main; + } + + +public: + static void* operator new(size_t UNUSED(count)) + { + return ThreadState::allocator.allocate(1); + } + + static void operator delete(void* ptr) + { + return ThreadState::allocator.deallocate(static_cast(ptr), + 1); + } + + static void init() + { + ThreadState::get_referrers_name = "get_referrers"; + ThreadState::_clocks_used_doing_gc = 0; + } + + ThreadState() + { + +#ifdef GREENLET_NEEDS_EXCEPTION_STATE_SAVED + this->exception_state = slp_get_exception_state(); +#endif + + // XXX: Potentially dangerous, exposing a not fully + // constructed object. + MainGreenlet* const main = this->alloc_main(); + this->main_greenlet = OwnedMainGreenlet::consuming( + main->self() + ); + assert(this->main_greenlet); + this->current_greenlet = main->self(); + // The main greenlet starts with 1 refs: The returned one. We + // then copied it to the current greenlet. + assert(this->main_greenlet.REFCNT() == 2); + } + + inline void restore_exception_state() + { +#ifdef GREENLET_NEEDS_EXCEPTION_STATE_SAVED + // It's probably important this be inlined and only call C + // functions to avoid adding an SEH frame. + slp_set_exception_state(this->exception_state); +#endif + } + + inline bool has_main_greenlet() const noexcept + { + return bool(this->main_greenlet); + } + + // Called from the ThreadStateCreator when we're in non-standard + // threading mode. In that case, there is an object in the Python + // thread state dictionary that points to us. The main greenlet + // also traverses into us, in which case it's crucial not to + // traverse back into the main greenlet. + int tp_traverse(visitproc visit, void* arg, bool traverse_main=true) + { + if (traverse_main) { + Py_VISIT(main_greenlet.borrow_o()); + } + if (traverse_main || current_greenlet != main_greenlet) { + Py_VISIT(current_greenlet.borrow_o()); + } + Py_VISIT(tracefunc.borrow()); + return 0; + } + + inline BorrowedMainGreenlet borrow_main_greenlet() const noexcept + { + assert(this->main_greenlet); + assert(this->main_greenlet.REFCNT() >= 2); + return this->main_greenlet; + }; + + inline OwnedMainGreenlet get_main_greenlet() const noexcept + { + return this->main_greenlet; + } + + /** + * In addition to returning a new reference to the currunt + * greenlet, this performs any maintenance needed. + */ + inline OwnedGreenlet get_current() + { + /* green_dealloc() cannot delete greenlets from other threads, so + it stores them in the thread dict; delete them now. */ + this->clear_deleteme_list(); + //assert(this->current_greenlet->main_greenlet == this->main_greenlet); + //assert(this->main_greenlet->main_greenlet == this->main_greenlet); + return this->current_greenlet; + } + + /** + * As for non-const get_current(); + */ + inline BorrowedGreenlet borrow_current() + { + this->clear_deleteme_list(); + return this->current_greenlet; + } + + /** + * Does no maintenance. + */ + inline OwnedGreenlet get_current() const + { + return this->current_greenlet; + } + + template + inline bool is_current(const refs::PyObjectPointer& obj) const + { + return this->current_greenlet.borrow_o() == obj.borrow_o(); + } + + inline void set_current(const OwnedGreenlet& target) + { + this->current_greenlet = target; + } + +private: + /** + * Deref and remove the greenlets from the deleteme list. Must be + * holding the GIL. + * + * If *murder* is true, then we must be called from a different + * thread than the one that these greenlets were running in. + * In that case, if the greenlet was actually running, we destroy + * the frame reference and otherwise make it appear dead before + * proceeding; otherwise, we would try (and fail) to raise an + * exception in it and wind up right back in this list. + */ + inline void clear_deleteme_list(const bool murder=false) + { + if (!this->deleteme.empty()) { + // It's possible we could add items to this list while + // running Python code if there's a thread switch, so we + // need to defensively copy it before that can happen. + deleteme_t copy = this->deleteme; + this->deleteme.clear(); // in case things come back on the list + for(deleteme_t::iterator it = copy.begin(), end = copy.end(); + it != end; + ++it ) { + PyGreenlet* to_del = *it; + if (murder) { + // Force each greenlet to appear dead; we can't raise an + // exception into it anymore anyway. + to_del->pimpl->murder_in_place(); + } + + // The only reference to these greenlets should be in + // this list, decreffing them should let them be + // deleted again, triggering calls to green_dealloc() + // in the correct thread (if we're not murdering). + // This may run arbitrary Python code and switch + // threads or greenlets! + Py_DECREF(to_del); + if (PyErr_Occurred()) { + PyErr_WriteUnraisable(nullptr); + PyErr_Clear(); + } + } + } + } + +public: + + /** + * Returns a new reference, or a false object. + */ + inline OwnedObject get_tracefunc() const + { + return tracefunc; + }; + + + inline void set_tracefunc(BorrowedObject tracefunc) + { + assert(tracefunc); + if (tracefunc == BorrowedObject(Py_None)) { + this->tracefunc.CLEAR(); + } + else { + this->tracefunc = tracefunc; + } + } + + /** + * Given a reference to a greenlet that some other thread + * attempted to delete (has a refcount of 0) store it for later + * deletion when the thread this state belongs to is current. + */ + inline void delete_when_thread_running(PyGreenlet* to_del) + { + Py_INCREF(to_del); + this->deleteme.push_back(to_del); + } + + /** + * Set to std::clock_t(-1) to disable. + */ + inline static std::clock_t& clocks_used_doing_gc() + { + return ThreadState::_clocks_used_doing_gc; + } + + ~ThreadState() + { + if (!PyInterpreterState_Head()) { + // We shouldn't get here (our callers protect us) + // but if we do, all we can do is bail early. + return; + } + + // We should not have an "origin" greenlet; that only exists + // for the temporary time during a switch, which should not + // be in progress as the thread dies. + //assert(!this->switching_state.origin); + + this->tracefunc.CLEAR(); + + // Forcibly GC as much as we can. + this->clear_deleteme_list(true); + + // The pending call did this. + assert(this->main_greenlet->thread_state() == nullptr); + + // If the main greenlet is the current greenlet, + // then we "fell off the end" and the thread died. + // It's possible that there is some other greenlet that + // switched to us, leaving a reference to the main greenlet + // on the stack, somewhere uncollectible. Try to detect that. + if (this->current_greenlet == this->main_greenlet && this->current_greenlet) { + assert(this->current_greenlet->is_currently_running_in_some_thread()); + // Drop one reference we hold. + this->current_greenlet.CLEAR(); + assert(!this->current_greenlet); + // Only our reference to the main greenlet should be left, + // But hold onto the pointer in case we need to do extra cleanup. + PyGreenlet* old_main_greenlet = this->main_greenlet.borrow(); + Py_ssize_t cnt = this->main_greenlet.REFCNT(); + this->main_greenlet.CLEAR(); + if (ThreadState::_clocks_used_doing_gc != std::clock_t(-1) + && cnt == 2 && Py_REFCNT(old_main_greenlet) == 1) { + // Highly likely that the reference is somewhere on + // the stack, not reachable by GC. Verify. + // XXX: This is O(n) in the total number of objects. + // TODO: Add a way to disable this at runtime, and + // another way to report on it. + std::clock_t begin = std::clock(); + NewReference gc(PyImport_ImportModule("gc")); + if (gc) { + OwnedObject get_referrers = gc.PyRequireAttr(ThreadState::get_referrers_name); + OwnedList refs(get_referrers.PyCall(old_main_greenlet)); + if (refs && refs.empty()) { + assert(refs.REFCNT() == 1); + // We found nothing! So we left a dangling + // reference: Probably the last thing some + // other greenlet did was call + // 'getcurrent().parent.switch()' to switch + // back to us. Clean it up. This will be the + // case on CPython 3.7 and newer, as they use + // an internal calling conversion that avoids + // creating method objects and storing them on + // the stack. + Py_DECREF(old_main_greenlet); + } + else if (refs + && refs.size() == 1 + && PyCFunction_Check(refs.at(0)) + && Py_REFCNT(refs.at(0)) == 2) { + assert(refs.REFCNT() == 1); + // Ok, we found a C method that refers to the + // main greenlet, and its only referenced + // twice, once in the list we just created, + // once from...somewhere else. If we can't + // find where else, then this is a leak. + // This happens in older versions of CPython + // that create a bound method object somewhere + // on the stack that we'll never get back to. + if (PyCFunction_GetFunction(refs.at(0).borrow()) == (PyCFunction)green_switch) { + BorrowedObject function_w = refs.at(0); + refs.clear(); // destroy the reference + // from the list. + // back to one reference. Can *it* be + // found? + assert(function_w.REFCNT() == 1); + refs = get_referrers.PyCall(function_w); + if (refs && refs.empty()) { + // Nope, it can't be found so it won't + // ever be GC'd. Drop it. + Py_CLEAR(function_w); + } + } + } + std::clock_t end = std::clock(); + ThreadState::_clocks_used_doing_gc += (end - begin); + } + } + } + + // We need to make sure this greenlet appears to be dead, + // because otherwise deallocing it would fail to raise an + // exception in it (the thread is dead) and put it back in our + // deleteme list. + if (this->current_greenlet) { + this->current_greenlet->murder_in_place(); + this->current_greenlet.CLEAR(); + } + + if (this->main_greenlet) { + // Couldn't have been the main greenlet that was running + // when the thread exited (because we already cleared this + // pointer if it was). This shouldn't be possible? + + // If the main greenlet was current when the thread died (it + // should be, right?) then we cleared its self pointer above + // when we cleared the current greenlet's main greenlet pointer. + // assert(this->main_greenlet->main_greenlet == this->main_greenlet + // || !this->main_greenlet->main_greenlet); + // // self reference, probably gone + // this->main_greenlet->main_greenlet.CLEAR(); + + // This will actually go away when the ivar is destructed. + this->main_greenlet.CLEAR(); + } + + if (PyErr_Occurred()) { + PyErr_WriteUnraisable(NULL); + PyErr_Clear(); + } + + } + +}; + +ImmortalString ThreadState::get_referrers_name(nullptr); +PythonAllocator ThreadState::allocator; +std::clock_t ThreadState::_clocks_used_doing_gc(0); + + + + + +}; // namespace greenlet + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TThreadStateCreator.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/TThreadStateCreator.hpp new file mode 100644 index 0000000..2ec7ab5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TThreadStateCreator.hpp @@ -0,0 +1,102 @@ +#ifndef GREENLET_THREAD_STATE_CREATOR_HPP +#define GREENLET_THREAD_STATE_CREATOR_HPP + +#include +#include + +#include "greenlet_internal.hpp" +#include "greenlet_refs.hpp" +#include "greenlet_thread_support.hpp" + +#include "TThreadState.hpp" + +namespace greenlet { + + +typedef void (*ThreadStateDestructor)(ThreadState* const); + +template +class ThreadStateCreator +{ +private: + // Initialized to 1, and, if still 1, created on access. + // Set to 0 on destruction. + ThreadState* _state; + G_NO_COPIES_OF_CLS(ThreadStateCreator); + + inline bool has_initialized_state() const noexcept + { + return this->_state != (ThreadState*)1; + } + + inline bool has_state() const noexcept + { + return this->has_initialized_state() && this->_state != nullptr; + } + +public: + + // Only one of these, auto created per thread. + // Constructing the state constructs the MainGreenlet. + ThreadStateCreator() : + _state((ThreadState*)1) + { + } + + ~ThreadStateCreator() + { + if (this->has_state()) { + Destructor(this->_state); + } + + this->_state = nullptr; + } + + inline ThreadState& state() + { + // The main greenlet will own this pointer when it is created, + // which will be right after this. The plan is to give every + // greenlet a pointer to the main greenlet for the thread it + // runs in; if we are doing something cross-thread, we need to + // access the pointer from the main greenlet. Deleting the + // thread, and hence the thread-local storage, will delete the + // state pointer in the main greenlet. + if (!this->has_initialized_state()) { + // XXX: Assuming allocation never fails + this->_state = new ThreadState; + // For non-standard threading, we need to store an object + // in the Python thread state dictionary so that it can be + // DECREF'd when the thread ends (ideally; the dict could + // last longer) and clean this object up. + } + if (!this->_state) { + throw std::runtime_error("Accessing state after destruction."); + } + return *this->_state; + } + + operator ThreadState&() + { + return this->state(); + } + + operator ThreadState*() + { + return &this->state(); + } + + inline int tp_traverse(visitproc visit, void* arg) + { + if (this->has_state()) { + return this->_state->tp_traverse(visit, arg); + } + return 0; + } + +}; + + + +}; // namespace greenlet + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TThreadStateDestroy.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/TThreadStateDestroy.cpp new file mode 100644 index 0000000..449b788 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TThreadStateDestroy.cpp @@ -0,0 +1,217 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of the ThreadState destructors. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#ifndef T_THREADSTATE_DESTROY +#define T_THREADSTATE_DESTROY + +#include "TGreenlet.hpp" + +#include "greenlet_thread_support.hpp" +#include "greenlet_compiler_compat.hpp" +#include "TGreenletGlobals.cpp" +#include "TThreadState.hpp" +#include "TThreadStateCreator.hpp" + +namespace greenlet { + +extern "C" { + +struct ThreadState_DestroyNoGIL +{ + /** + This function uses the same lock that the PendingCallback does + */ + static void + MarkGreenletDeadAndQueueCleanup(ThreadState* const state) + { +#if GREENLET_BROKEN_THREAD_LOCAL_CLEANUP_JUST_LEAK + return; +#endif + // We are *NOT* holding the GIL. Our thread is in the middle + // of its death throes and the Python thread state is already + // gone so we can't use most Python APIs. One that is safe is + // ``Py_AddPendingCall``, unless the interpreter itself has + // been torn down. There is a limited number of calls that can + // be queued: 32 (NPENDINGCALLS) in CPython 3.10, so we + // coalesce these calls using our own queue. + + if (!MarkGreenletDeadIfNeeded(state)) { + // No state, or no greenlet + return; + } + + // XXX: Because we don't have the GIL, this is a race condition. + if (!PyInterpreterState_Head()) { + // We have to leak the thread state, if the + // interpreter has shut down when we're getting + // deallocated, we can't run the cleanup code that + // deleting it would imply. + return; + } + + AddToCleanupQueue(state); + + } + +private: + + // If the state has an allocated main greenlet: + // - mark the greenlet as dead by disassociating it from the state; + // - return 1 + // Otherwise, return 0. + static bool + MarkGreenletDeadIfNeeded(ThreadState* const state) + { + if (state && state->has_main_greenlet()) { + // mark the thread as dead ASAP. + // this is racy! If we try to throw or switch to a + // greenlet from this thread from some other thread before + // we clear the state pointer, it won't realize the state + // is dead which can crash the process. + PyGreenlet* p(state->borrow_main_greenlet().borrow()); + assert(p->pimpl->thread_state() == state || p->pimpl->thread_state() == nullptr); + dynamic_cast(p->pimpl)->thread_state(nullptr); + return true; + } + return false; + } + + static void + AddToCleanupQueue(ThreadState* const state) + { + assert(state && state->has_main_greenlet()); + + // NOTE: Because we're not holding the GIL here, some other + // Python thread could run and call ``os.fork()``, which would + // be bad if that happened while we are holding the cleanup + // lock (it wouldn't function in the child process). + // Make a best effort to try to keep the duration we hold the + // lock short. + // TODO: On platforms that support it, use ``pthread_atfork`` to + // drop this lock. + LockGuard cleanup_lock(*mod_globs->thread_states_to_destroy_lock); + + mod_globs->queue_to_destroy(state); + if (mod_globs->thread_states_to_destroy.size() == 1) { + // We added the first item to the queue. We need to schedule + // the cleanup. + + // A size greater than 1 means that we have already added the pending call, + // and in fact, it may be executing now. + // If it is executing, our lock makes sure that it will see the item we just added + // to the queue on its next iteration (after we release the lock) + // + // A size of 1 means there is no pending call, OR the pending call is + // currently executing, has dropped the lock, and is deleting the last item + // from the queue; its next iteration will go ahead and delete the item we just added. + // And the pending call we schedule here will have no work to do. + int result = AddPendingCall( + PendingCallback_DestroyQueueWithGIL, + nullptr); + if (result < 0) { + // Hmm, what can we do here? + fprintf(stderr, + "greenlet: WARNING: failed in call to Py_AddPendingCall; " + "expect a memory leak.\n"); + } + } + } + + static int + PendingCallback_DestroyQueueWithGIL(void* UNUSED(arg)) + { + // We're holding the GIL here, so no Python code should be able to + // run to call ``os.fork()``. + while (1) { + ThreadState* to_destroy; + { + LockGuard cleanup_lock(*mod_globs->thread_states_to_destroy_lock); + if (mod_globs->thread_states_to_destroy.empty()) { + break; + } + to_destroy = mod_globs->take_next_to_destroy(); + } + assert(to_destroy); + assert(to_destroy->has_main_greenlet()); + // Drop the lock while we do the actual deletion. + // This allows other calls to MarkGreenletDeadAndQueueCleanup + // to enter and add to our queue. + DestroyOneWithGIL(to_destroy); + } + return 0; + } + + static void + DestroyOneWithGIL(const ThreadState* const state) + { + // Holding the GIL. + // Passed a non-shared pointer to the actual thread state. + // state -> main greenlet + assert(state->has_main_greenlet()); + PyGreenlet* main(state->borrow_main_greenlet()); + // When we need to do cross-thread operations, we check this. + // A NULL value means the thread died some time ago. + // We do this here, rather than in a Python dealloc function + // for the greenlet, in case there's still a reference out + // there. + dynamic_cast(main->pimpl)->thread_state(nullptr); + + delete state; // Deleting this runs the destructor, DECREFs the main greenlet. + } + + + static int AddPendingCall(int (*func)(void*), void* arg) + { + // If the interpreter is in the middle of finalizing, we can't add a + // pending call. Trying to do so will end up in a SIGSEGV, as + // Py_AddPendingCall will not be able to get the interpreter and will + // try to dereference a NULL pointer. It's possible this can still + // segfault if we happen to get context switched, and maybe we should + // just always implement our own AddPendingCall, but I'd like to see if + // this works first +#if GREENLET_PY313 + if (Py_IsFinalizing()) { +#else + if (_Py_IsFinalizing()) { +#endif +#ifdef GREENLET_DEBUG + // No need to log in the general case. Yes, we'll leak, + // but we're shutting down so it should be ok. + fprintf(stderr, + "greenlet: WARNING: Interpreter is finalizing. Ignoring " + "call to Py_AddPendingCall; \n"); +#endif + return 0; + } + return Py_AddPendingCall(func, arg); + } + + + + + +}; +}; + +}; // namespace greenlet + +// The intent when GET_THREAD_STATE() is needed multiple times in a +// function is to take a reference to its return value in a local +// variable, to avoid the thread-local indirection. On some platforms +// (macOS), accessing a thread-local involves a function call (plus an +// initial function call in each function that uses a thread local); +// in contrast, static volatile variables are at some pre-computed +// offset. +typedef greenlet::ThreadStateCreator ThreadStateCreator; +static thread_local ThreadStateCreator g_thread_state_global; +#define GET_THREAD_STATE() g_thread_state_global + +#endif //T_THREADSTATE_DESTROY diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/TUserGreenlet.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/TUserGreenlet.cpp new file mode 100644 index 0000000..73a8133 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/TUserGreenlet.cpp @@ -0,0 +1,662 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/** + * Implementation of greenlet::UserGreenlet. + * + * Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#ifndef T_USER_GREENLET_CPP +#define T_USER_GREENLET_CPP + +#include "greenlet_internal.hpp" +#include "TGreenlet.hpp" + +#include "TThreadStateDestroy.cpp" + + +namespace greenlet { +using greenlet::refs::BorrowedMainGreenlet; +greenlet::PythonAllocator UserGreenlet::allocator; + +void* UserGreenlet::operator new(size_t UNUSED(count)) +{ + return allocator.allocate(1); +} + + +void UserGreenlet::operator delete(void* ptr) +{ + return allocator.deallocate(static_cast(ptr), + 1); +} + + +UserGreenlet::UserGreenlet(PyGreenlet* p, BorrowedGreenlet the_parent) + : Greenlet(p), _parent(the_parent) +{ +} + +UserGreenlet::~UserGreenlet() +{ + // Python 3.11: If we don't clear out the raw frame datastack + // when deleting an unfinished greenlet, + // TestLeaks.test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_main fails. + this->python_state.did_finish(nullptr); + this->tp_clear(); +} + + +const BorrowedMainGreenlet +UserGreenlet::main_greenlet() const +{ + return this->_main_greenlet; +} + + +BorrowedMainGreenlet +UserGreenlet::find_main_greenlet_in_lineage() const +{ + if (this->started()) { + assert(this->_main_greenlet); + return BorrowedMainGreenlet(this->_main_greenlet); + } + + if (!this->_parent) { + /* garbage collected greenlet in chain */ + // XXX: WHAT? + return BorrowedMainGreenlet(nullptr); + } + + return this->_parent->find_main_greenlet_in_lineage(); +} + + +/** + * CAUTION: This will allocate memory and may trigger garbage + * collection and arbitrary Python code. + */ +OwnedObject +UserGreenlet::throw_GreenletExit_during_dealloc(const ThreadState& current_thread_state) +{ + /* The dying greenlet cannot be a parent of ts_current + because the 'parent' field chain would hold a + reference */ + UserGreenlet::ParentIsCurrentGuard with_current_parent(this, current_thread_state); + + // We don't care about the return value, only whether an + // exception happened. Whether or not an exception happens, + // we need to restore the parent in case the greenlet gets + // resurrected. + return Greenlet::throw_GreenletExit_during_dealloc(current_thread_state); +} + +ThreadState* +UserGreenlet::thread_state() const noexcept +{ + // TODO: maybe make this throw, if the thread state isn't there? + // if (!this->main_greenlet) { + // throw std::runtime_error("No thread state"); // TODO: Better exception + // } + if (!this->_main_greenlet) { + return nullptr; + } + return this->_main_greenlet->thread_state(); +} + + +bool +UserGreenlet::was_running_in_dead_thread() const noexcept +{ + return this->_main_greenlet && !this->thread_state(); +} + +OwnedObject +UserGreenlet::g_switch() +{ + assert(this->args() || PyErr_Occurred()); + + try { + this->check_switch_allowed(); + } + catch (const PyErrOccurred&) { + this->release_args(); + throw; + } + + // Switching greenlets used to attempt to clean out ones that need + // deleted *if* we detected a thread switch. Should it still do + // that? + // An issue is that if we delete a greenlet from another thread, + // it gets queued to this thread, and ``kill_greenlet()`` switches + // back into the greenlet + + /* find the real target by ignoring dead greenlets, + and if necessary starting a greenlet. */ + switchstack_result_t err; + Greenlet* target = this; + // TODO: probably cleaner to handle the case where we do + // switch to ourself separately from the other cases. + // This can probably even further be simplified if we keep + // track of the switching_state we're going for and just call + // into g_switch() if it's not ourself. The main problem with that + // is that we would be using more stack space. + bool target_was_me = true; + bool was_initial_stub = false; + while (target) { + if (target->active()) { + if (!target_was_me) { + target->args() <<= this->args(); + assert(!this->args()); + } + err = target->g_switchstack(); + break; + } + if (!target->started()) { + // We never encounter a main greenlet that's not started. + assert(!target->main()); + UserGreenlet* real_target = static_cast(target); + assert(real_target); + void* dummymarker; + was_initial_stub = true; + if (!target_was_me) { + target->args() <<= this->args(); + assert(!this->args()); + } + try { + // This can only throw back to us while we're + // still in this greenlet. Once the new greenlet + // is bootstrapped, it has its own exception state. + err = real_target->g_initialstub(&dummymarker); + } + catch (const PyErrOccurred&) { + this->release_args(); + throw; + } + catch (const GreenletStartedWhileInPython&) { + // The greenlet was started sometime before this + // greenlet actually switched to it, i.e., + // "concurrent" calls to switch() or throw(). + // We need to retry the switch. + // Note that the current greenlet has been reset + // to this one (or we wouldn't be running!) + continue; + } + break; + } + + target = target->parent(); + target_was_me = false; + } + // The ``this`` pointer and all other stack or register based + // variables are invalid now, at least where things succeed + // above. + // But this one, probably not so much? It's not clear if it's + // safe to throw an exception at this point. + + if (err.status < 0) { + // If we get here, either g_initialstub() + // failed, or g_switchstack() failed. Either one of those + // cases SHOULD leave us in the original greenlet with a valid + // stack. + return this->on_switchstack_or_initialstub_failure(target, err, target_was_me, was_initial_stub); + } + + // err.the_new_current_greenlet would be the same as ``target``, + // if target wasn't probably corrupt. + return err.the_new_current_greenlet->g_switch_finish(err); +} + + + +Greenlet::switchstack_result_t +UserGreenlet::g_initialstub(void* mark) +{ + OwnedObject run; + + // We need to grab a reference to the current switch arguments + // in case we're entered concurrently during the call to + // GetAttr() and have to try again. + // We'll restore them when we return in that case. + // Scope them tightly to avoid ref leaks. + { + SwitchingArgs args(this->args()); + + /* save exception in case getattr clears it */ + PyErrPieces saved; + + /* + self.run is the object to call in the new greenlet. + This could run arbitrary python code and switch greenlets! + */ + run = this->self().PyRequireAttr(mod_globs->str_run); + /* restore saved exception */ + saved.PyErrRestore(); + + + /* recheck that it's safe to switch in case greenlet reparented anywhere above */ + this->check_switch_allowed(); + + /* by the time we got here another start could happen elsewhere, + * that means it should now be a regular switch. + * This can happen if the Python code is a subclass that implements + * __getattribute__ or __getattr__, or makes ``run`` a descriptor; + * all of those can run arbitrary code that switches back into + * this greenlet. + */ + if (this->stack_state.started()) { + // the successful switch cleared these out, we need to + // restore our version. They will be copied on up to the + // next target. + assert(!this->args()); + this->args() <<= args; + throw GreenletStartedWhileInPython(); + } + } + + // Sweet, if we got here, we have the go-ahead and will switch + // greenlets. + // Nothing we do from here on out should allow for a thread or + // greenlet switch: No arbitrary calls to Python, including + // decref'ing + +#if GREENLET_USE_CFRAME + /* OK, we need it, we're about to switch greenlets, save the state. */ + /* + See green_new(). This is a stack-allocated variable used + while *self* is in PyObject_Call(). + We want to defer copying the state info until we're sure + we need it and are in a stable place to do so. + */ + _PyCFrame trace_info; + + this->python_state.set_new_cframe(trace_info); +#endif + /* start the greenlet */ + ThreadState& thread_state = GET_THREAD_STATE().state(); + this->stack_state = StackState(mark, + thread_state.borrow_current()->stack_state); + this->python_state.set_initial_state(PyThreadState_GET()); + this->exception_state.clear(); + this->_main_greenlet = thread_state.get_main_greenlet(); + + /* perform the initial switch */ + switchstack_result_t err = this->g_switchstack(); + /* returns twice! + The 1st time with ``err == 1``: we are in the new greenlet. + This one owns a greenlet that used to be current. + The 2nd time with ``err <= 0``: back in the caller's + greenlet; this happens if the child finishes or switches + explicitly to us. Either way, the ``err`` variable is + created twice at the same memory location, but possibly + having different ``origin`` values. Note that it's not + constructed for the second time until the switch actually happens. + */ + if (err.status == 1) { + // In the new greenlet. + + // This never returns! Calling inner_bootstrap steals + // the contents of our run object within this stack frame, so + // it is not valid to do anything with it. + try { + this->inner_bootstrap(err.origin_greenlet.relinquish_ownership(), + run.relinquish_ownership()); + } + // Getting a C++ exception here isn't good. It's probably a + // bug in the underlying greenlet, meaning it's probably a + // C++ extension. We're going to abort anyway, but try to + // display some nice information *if* possible. Some obscure + // platforms don't properly support this (old 32-bit Arm, see see + // https://github.com/python-greenlet/greenlet/issues/385); that's not + // great, but should usually be OK because, as mentioned above, we're + // terminating anyway. + // + // The catching is tested by + // ``test_cpp.CPPTests.test_unhandled_exception_in_greenlet_aborts``. + // + // PyErrOccurred can theoretically be thrown by + // inner_bootstrap() -> g_switch_finish(), but that should + // never make it back to here. It is a std::exception and + // would be caught if it is. + catch (const std::exception& e) { + std::string base = "greenlet: Unhandled C++ exception: "; + base += e.what(); + Py_FatalError(base.c_str()); + } + catch (...) { + // Some compilers/runtimes use exceptions internally. + // It appears that GCC on Linux with libstdc++ throws an + // exception internally at process shutdown time to unwind + // stacks and clean up resources. Depending on exactly + // where we are when the process exits, that could result + // in an unknown exception getting here. If we + // Py_FatalError() or abort() here, we interfere with + // orderly process shutdown. Throwing the exception on up + // is the right thing to do. + // + // gevent's ``examples/dns_mass_resolve.py`` demonstrates this. +#ifndef NDEBUG + fprintf(stderr, + "greenlet: inner_bootstrap threw unknown exception; " + "is the process terminating?\n"); +#endif + throw; + } + Py_FatalError("greenlet: inner_bootstrap returned with no exception.\n"); + } + + + // In contrast, notice that we're keeping the origin greenlet + // around as an owned reference; we need it to call the trace + // function for the switch back into the parent. It was only + // captured at the time the switch actually happened, though, + // so we haven't been keeping an extra reference around this + // whole time. + + /* back in the parent */ + if (err.status < 0) { + /* start failed badly, restore greenlet state */ + this->stack_state = StackState(); + this->_main_greenlet.CLEAR(); + // CAUTION: This may run arbitrary Python code. + run.CLEAR(); // inner_bootstrap didn't run, we own the reference. + } + + // In the success case, the spawned code (inner_bootstrap) will + // take care of decrefing this, so we relinquish ownership so as + // to not double-decref. + + run.relinquish_ownership(); + + return err; +} + + +void +UserGreenlet::inner_bootstrap(PyGreenlet* origin_greenlet, PyObject* run) +{ + // The arguments here would be another great place for move. + // As it is, we take them as a reference so that when we clear + // them we clear what's on the stack above us. Do that NOW, and + // without using a C++ RAII object, + // so there's no way that exiting the parent frame can clear it, + // or we clear it unexpectedly. This arises in the context of the + // interpreter shutting down. See https://github.com/python-greenlet/greenlet/issues/325 + //PyObject* run = _run.relinquish_ownership(); + + /* in the new greenlet */ + assert(this->thread_state()->borrow_current() == BorrowedGreenlet(this->_self)); + // C++ exceptions cannot propagate to the parent greenlet from + // here. (TODO: Do we need a catch(...) clause, perhaps on the + // function itself? ALl we could do is terminate the program.) + // NOTE: On 32-bit Windows, the call chain is extremely + // important here in ways that are subtle, having to do with + // the depth of the SEH list. The call to restore it MUST NOT + // add a new SEH handler to the list, or we'll restore it to + // the wrong thing. + this->thread_state()->restore_exception_state(); + /* stack variables from above are no good and also will not unwind! */ + // EXCEPT: That can't be true, we access run, among others, here. + + this->stack_state.set_active(); /* running */ + + // We're about to possibly run Python code again, which + // could switch back/away to/from us, so we need to grab the + // arguments locally. + SwitchingArgs args; + args <<= this->args(); + assert(!this->args()); + + // XXX: We could clear this much earlier, right? + // Or would that introduce the possibility of running Python + // code when we don't want to? + // CAUTION: This may run arbitrary Python code. + this->_run_callable.CLEAR(); + + + // The first switch we need to manually call the trace + // function here instead of in g_switch_finish, because we + // never return there. + if (OwnedObject tracefunc = this->thread_state()->get_tracefunc()) { + OwnedGreenlet trace_origin; + trace_origin = origin_greenlet; + try { + g_calltrace(tracefunc, + args ? mod_globs->event_switch : mod_globs->event_throw, + trace_origin, + this->_self); + } + catch (const PyErrOccurred&) { + /* Turn trace errors into switch throws */ + args.CLEAR(); + } + } + + // We no longer need the origin, it was only here for + // tracing. + // We may never actually exit this stack frame so we need + // to explicitly clear it. + // This could run Python code and switch. + Py_CLEAR(origin_greenlet); + + OwnedObject result; + if (!args) { + /* pending exception */ + result = NULL; + } + else { + /* call g.run(*args, **kwargs) */ + // This could result in further switches + try { + //result = run.PyCall(args.args(), args.kwargs()); + // CAUTION: Just invoking this, before the function even + // runs, may cause memory allocations, which may trigger + // GC, which may run arbitrary Python code. + result = OwnedObject::consuming(PyObject_Call(run, args.args().borrow(), args.kwargs().borrow())); + } + catch (...) { + // Unhandled C++ exception! + + // If we declare ourselves as noexcept, if we don't catch + // this here, most platforms will just abort() the + // process. But on 64-bit Windows with older versions of + // the C runtime, this can actually corrupt memory and + // just return. We see this when compiling with the + // Windows 7.0 SDK targeting Windows Server 2008, but not + // when using the Appveyor Visual Studio 2019 image. So + // this currently only affects Python 2.7 on Windows 64. + // That is, the tests pass and the runtime aborts + // everywhere else. + // + // However, if we catch it and try to continue with a + // Python error, then all Windows 64 bit platforms corrupt + // memory. So all we can do is manually abort, hopefully + // with a good error message. (Note that the above was + // tested WITHOUT the `/EHr` switch being used at compile + // time, so MSVC may have "optimized" out important + // checking. Using that switch, we may be in a better + // place in terms of memory corruption.) But sometimes it + // can't be caught here at all, which is confusing but not + // terribly surprising; so again, the G_NOEXCEPT_WIN32 + // plus "/EHr". + // + // Hopefully the basic C stdlib is still functional enough + // for us to at least print an error. + // + // It gets more complicated than that, though, on some + // platforms, specifically at least Linux/gcc/libstdc++. They use + // an exception to unwind the stack when a background + // thread exits. (See comments about noexcept.) So this + // may not actually represent anything untoward. On those + // platforms we allow throws of this to propagate, or + // attempt to anyway. +# if defined(WIN32) || defined(_WIN32) + Py_FatalError( + "greenlet: Unhandled C++ exception from a greenlet run function. " + "Because memory is likely corrupted, terminating process."); + std::abort(); +#else + throw; +#endif + } + } + // These lines may run arbitrary code + args.CLEAR(); + Py_CLEAR(run); + + if (!result + && mod_globs->PyExc_GreenletExit.PyExceptionMatches() + && (this->args())) { + // This can happen, for example, if our only reference + // goes away after we switch back to the parent. + // See test_dealloc_switch_args_not_lost + PyErrPieces clear_error; + result <<= this->args(); + result = single_result(result); + } + this->release_args(); + this->python_state.did_finish(PyThreadState_GET()); + + result = g_handle_exit(result); + assert(this->thread_state()->borrow_current() == this->_self); + + /* jump back to parent */ + this->stack_state.set_inactive(); /* dead */ + + + // TODO: Can we decref some things here? Release our main greenlet + // and maybe parent? + for (Greenlet* parent = this->_parent; + parent; + parent = parent->parent()) { + // We need to somewhere consume a reference to + // the result; in most cases we'll never have control + // back in this stack frame again. Calling + // green_switch actually adds another reference! + // This would probably be clearer with a specific API + // to hand results to the parent. + parent->args() <<= result; + assert(!result); + // The parent greenlet now owns the result; in the + // typical case we'll never get back here to assign to + // result and thus release the reference. + try { + result = parent->g_switch(); + } + catch (const PyErrOccurred&) { + // Ignore, keep passing the error on up. + } + + /* Return here means switch to parent failed, + * in which case we throw *current* exception + * to the next parent in chain. + */ + assert(!result); + } + /* We ran out of parents, cannot continue */ + PyErr_WriteUnraisable(this->self().borrow_o()); + Py_FatalError("greenlet: ran out of parent greenlets while propagating exception; " + "cannot continue"); + std::abort(); +} + +void +UserGreenlet::run(const BorrowedObject nrun) +{ + if (this->started()) { + throw AttributeError( + "run cannot be set " + "after the start of the greenlet"); + } + this->_run_callable = nrun; +} + +const OwnedGreenlet +UserGreenlet::parent() const +{ + return this->_parent; +} + +void +UserGreenlet::parent(const BorrowedObject raw_new_parent) +{ + if (!raw_new_parent) { + throw AttributeError("can't delete attribute"); + } + + BorrowedMainGreenlet main_greenlet_of_new_parent; + BorrowedGreenlet new_parent(raw_new_parent.borrow()); // could + // throw + // TypeError! + for (BorrowedGreenlet p = new_parent; p; p = p->parent()) { + if (p == this->self()) { + throw ValueError("cyclic parent chain"); + } + main_greenlet_of_new_parent = p->main_greenlet(); + } + + if (!main_greenlet_of_new_parent) { + throw ValueError("parent must not be garbage collected"); + } + + if (this->started() + && this->_main_greenlet != main_greenlet_of_new_parent) { + throw ValueError("parent cannot be on a different thread"); + } + + this->_parent = new_parent; +} + +void +UserGreenlet::murder_in_place() +{ + this->_main_greenlet.CLEAR(); + Greenlet::murder_in_place(); +} + +bool +UserGreenlet::belongs_to_thread(const ThreadState* thread_state) const +{ + return Greenlet::belongs_to_thread(thread_state) && this->_main_greenlet == thread_state->borrow_main_greenlet(); +} + + +int +UserGreenlet::tp_traverse(visitproc visit, void* arg) +{ + Py_VISIT(this->_parent.borrow_o()); + Py_VISIT(this->_main_greenlet.borrow_o()); + Py_VISIT(this->_run_callable.borrow_o()); + + return Greenlet::tp_traverse(visit, arg); +} + +int +UserGreenlet::tp_clear() +{ + Greenlet::tp_clear(); + this->_parent.CLEAR(); + this->_main_greenlet.CLEAR(); + this->_run_callable.CLEAR(); + return 0; +} + +UserGreenlet::ParentIsCurrentGuard::ParentIsCurrentGuard(UserGreenlet* p, + const ThreadState& thread_state) + : oldparent(p->_parent), + greenlet(p) +{ + p->_parent = thread_state.get_current(); +} + +UserGreenlet::ParentIsCurrentGuard::~ParentIsCurrentGuard() +{ + this->greenlet->_parent = oldparent; + oldparent.CLEAR(); +} + +}; //namespace greenlet +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/__init__.py b/netdeploy/lib/python3.11/site-packages/greenlet/__init__.py new file mode 100644 index 0000000..6401497 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/__init__.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +The root of the greenlet package. +""" +from __future__ import absolute_import +from __future__ import division +from __future__ import print_function + +__all__ = [ + '__version__', + '_C_API', + + 'GreenletExit', + 'error', + + 'getcurrent', + 'greenlet', + + 'gettrace', + 'settrace', +] + +# pylint:disable=no-name-in-module + +### +# Metadata +### +__version__ = '3.2.4' +from ._greenlet import _C_API # pylint:disable=no-name-in-module + +### +# Exceptions +### +from ._greenlet import GreenletExit +from ._greenlet import error + +### +# greenlets +### +from ._greenlet import getcurrent +from ._greenlet import greenlet + +### +# tracing +### +try: + from ._greenlet import gettrace + from ._greenlet import settrace +except ImportError: + # Tracing wasn't supported. + # XXX: The option to disable it was removed in 1.0, + # so this branch should be dead code. + pass + +### +# Constants +# These constants aren't documented and aren't recommended. +# In 1.0, USE_GC and USE_TRACING are always true, and USE_CONTEXT_VARS +# is the same as ``sys.version_info[:2] >= 3.7`` +### +from ._greenlet import GREENLET_USE_CONTEXT_VARS # pylint:disable=unused-import +from ._greenlet import GREENLET_USE_GC # pylint:disable=unused-import +from ._greenlet import GREENLET_USE_TRACING # pylint:disable=unused-import + +# Controlling the use of the gc module. Provisional API for this greenlet +# implementation in 2.0. +from ._greenlet import CLOCKS_PER_SEC # pylint:disable=unused-import +from ._greenlet import enable_optional_cleanup # pylint:disable=unused-import +from ._greenlet import get_clocks_used_doing_optional_cleanup # pylint:disable=unused-import + +# Other APIS in the _greenlet module are for test support. diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/_greenlet.cpython-311-x86_64-linux-gnu.so b/netdeploy/lib/python3.11/site-packages/greenlet/_greenlet.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..3ff743cad8d6f0f264fe7ddaeef7776db6bc0a2e GIT binary patch literal 1365232 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%0A_Z#+01S{#i;8bwh^*bi*eP4SNz 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

rejrbC#nL}^zKRe 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%0D$-K{PM*VrAo~qLBjWRllEwub!{)_s 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

%(reason)s

+ %(mesg)s + + + """) % {"reason": reason, "mesg": html.escape(mesg)} + + http = textwrap.dedent("""\ + HTTP/1.1 %s %s\r + Connection: close\r + Content-Type: text/html\r + Content-Length: %d\r + \r + %s""") % (str(status_int), reason, len(html_error), html_error) + write_nonblock(sock, http.encode('latin1')) + + +def _called_with_wrong_args(f): + """Check whether calling a function raised a ``TypeError`` because + the call failed or because something in the function raised the + error. + + :param f: The function that was called. + :return: ``True`` if the call failed. + """ + tb = sys.exc_info()[2] + + try: + while tb is not None: + if tb.tb_frame.f_code is f.__code__: + # In the function, it was called successfully. + return False + + tb = tb.tb_next + + # Didn't reach the function. + return True + finally: + # Delete tb to break a circular reference in Python 2. + # https://docs.python.org/2/library/sys.html#sys.exc_info + del tb + + +def import_app(module): + parts = module.split(":", 1) + if len(parts) == 1: + obj = "application" + else: + module, obj = parts[0], parts[1] + + try: + mod = importlib.import_module(module) + except ImportError: + if module.endswith(".py") and os.path.exists(module): + msg = "Failed to find application, did you mean '%s:%s'?" + raise ImportError(msg % (module.rsplit(".", 1)[0], obj)) + raise + + # Parse obj as a single expression to determine if it's a valid + # attribute name or function call. + try: + expression = ast.parse(obj, mode="eval").body + except SyntaxError: + raise AppImportError( + "Failed to parse %r as an attribute name or function call." % obj + ) + + if isinstance(expression, ast.Name): + name = expression.id + args = kwargs = None + elif isinstance(expression, ast.Call): + # Ensure the function name is an attribute name only. + if not isinstance(expression.func, ast.Name): + raise AppImportError("Function reference must be a simple name: %r" % obj) + + name = expression.func.id + + # Parse the positional and keyword arguments as literals. + try: + args = [ast.literal_eval(arg) for arg in expression.args] + kwargs = {kw.arg: ast.literal_eval(kw.value) for kw in expression.keywords} + except ValueError: + # literal_eval gives cryptic error messages, show a generic + # message with the full expression instead. + raise AppImportError( + "Failed to parse arguments as literal values: %r" % obj + ) + else: + raise AppImportError( + "Failed to parse %r as an attribute name or function call." % obj + ) + + is_debug = logging.root.level == logging.DEBUG + try: + app = getattr(mod, name) + except AttributeError: + if is_debug: + traceback.print_exception(*sys.exc_info()) + raise AppImportError("Failed to find attribute %r in %r." % (name, module)) + + # If the expression was a function call, call the retrieved object + # to get the real application. + if args is not None: + try: + app = app(*args, **kwargs) + except TypeError as e: + # If the TypeError was due to bad arguments to the factory + # function, show Python's nice error message without a + # traceback. + if _called_with_wrong_args(app): + raise AppImportError( + "".join(traceback.format_exception_only(TypeError, e)).strip() + ) + + # Otherwise it was raised from within the function, show the + # full traceback. + raise + + if app is None: + raise AppImportError("Failed to find application object: %r" % obj) + + if not callable(app): + raise AppImportError("Application object must be callable.") + return app + + +def getcwd(): + # get current path, try to use PWD env first + try: + a = os.stat(os.environ['PWD']) + b = os.stat(os.getcwd()) + if a.st_ino == b.st_ino and a.st_dev == b.st_dev: + cwd = os.environ['PWD'] + else: + cwd = os.getcwd() + except Exception: + cwd = os.getcwd() + return cwd + + +def http_date(timestamp=None): + """Return the current date and time formatted for a message header.""" + if timestamp is None: + timestamp = time.time() + s = email.utils.formatdate(timestamp, localtime=False, usegmt=True) + return s + + +def is_hoppish(header): + return header.lower().strip() in hop_headers + + +def daemonize(enable_stdio_inheritance=False): + """\ + Standard daemonization of a process. + http://www.faqs.org/faqs/unix-faq/programmer/faq/ section 1.7 + """ + if 'GUNICORN_FD' not in os.environ: + if os.fork(): + os._exit(0) + os.setsid() + + if os.fork(): + os._exit(0) + + os.umask(0o22) + + # In both the following any file descriptors above stdin + # stdout and stderr are left untouched. The inheritance + # option simply allows one to have output go to a file + # specified by way of shell redirection when not wanting + # to use --error-log option. + + if not enable_stdio_inheritance: + # Remap all of stdin, stdout and stderr on to + # /dev/null. The expectation is that users have + # specified the --error-log option. + + closerange(0, 3) + + fd_null = os.open(REDIRECT_TO, os.O_RDWR) + # PEP 446, make fd for /dev/null inheritable + os.set_inheritable(fd_null, True) + + # expect fd_null to be always 0 here, but in-case not ... + if fd_null != 0: + os.dup2(fd_null, 0) + + os.dup2(fd_null, 1) + os.dup2(fd_null, 2) + + else: + fd_null = os.open(REDIRECT_TO, os.O_RDWR) + + # Always redirect stdin to /dev/null as we would + # never expect to need to read interactive input. + + if fd_null != 0: + os.close(0) + os.dup2(fd_null, 0) + + # If stdout and stderr are still connected to + # their original file descriptors we check to see + # if they are associated with terminal devices. + # When they are we map them to /dev/null so that + # are still detached from any controlling terminal + # properly. If not we preserve them as they are. + # + # If stdin and stdout were not hooked up to the + # original file descriptors, then all bets are + # off and all we can really do is leave them as + # they were. + # + # This will allow 'gunicorn ... > output.log 2>&1' + # to work with stdout/stderr going to the file + # as expected. + # + # Note that if using --error-log option, the log + # file specified through shell redirection will + # only be used up until the log file specified + # by the option takes over. As it replaces stdout + # and stderr at the file descriptor level, then + # anything using stdout or stderr, including having + # cached a reference to them, will still work. + + def redirect(stream, fd_expect): + try: + fd = stream.fileno() + if fd == fd_expect and stream.isatty(): + os.close(fd) + os.dup2(fd_null, fd) + except AttributeError: + pass + + redirect(sys.stdout, 1) + redirect(sys.stderr, 2) + + +def seed(): + try: + random.seed(os.urandom(64)) + except NotImplementedError: + random.seed('%s.%s' % (time.time(), os.getpid())) + + +def check_is_writable(path): + try: + with open(path, 'a') as f: + f.close() + except OSError as e: + raise RuntimeError("Error: '%s' isn't writable [%r]" % (path, e)) + + +def to_bytestring(value, encoding="utf8"): + """Converts a string argument to a byte string""" + if isinstance(value, bytes): + return value + if not isinstance(value, str): + raise TypeError('%r is not a string' % value) + + return value.encode(encoding) + + +def has_fileno(obj): + if not hasattr(obj, "fileno"): + return False + + # check BytesIO case and maybe others + try: + obj.fileno() + except (AttributeError, OSError, io.UnsupportedOperation): + return False + + return True + + +def warn(msg): + print("!!!", file=sys.stderr) + + lines = msg.splitlines() + for i, line in enumerate(lines): + if i == 0: + line = "WARNING: %s" % line + print("!!! %s" % line, file=sys.stderr) + + print("!!!\n", file=sys.stderr) + sys.stderr.flush() + + +def make_fail_app(msg): + msg = to_bytestring(msg) + + def app(environ, start_response): + start_response("500 Internal Server Error", [ + ("Content-Type", "text/plain"), + ("Content-Length", str(len(msg))) + ]) + return [msg] + + return app + + +def split_request_uri(uri): + if uri.startswith("//"): + # When the path starts with //, urlsplit considers it as a + # relative uri while the RFC says we should consider it as abs_path + # http://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html#sec5.1.2 + # We use temporary dot prefix to workaround this behaviour + parts = urllib.parse.urlsplit("." + uri) + return parts._replace(path=parts.path[1:]) + + return urllib.parse.urlsplit(uri) + + +# From six.reraise +def reraise(tp, value, tb=None): + try: + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None + tb = None + + +def bytes_to_str(b): + if isinstance(b, str): + return b + return str(b, 'latin1') + + +def unquote_to_wsgi_str(string): + return urllib.parse.unquote_to_bytes(string).decode('latin-1') diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/workers/__init__.py b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/__init__.py new file mode 100644 index 0000000..3da5f85 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/__init__.py @@ -0,0 +1,14 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +# supported gunicorn workers. +SUPPORTED_WORKERS = { + "sync": "gunicorn.workers.sync.SyncWorker", + "eventlet": "gunicorn.workers.geventlet.EventletWorker", + "gevent": "gunicorn.workers.ggevent.GeventWorker", + "gevent_wsgi": "gunicorn.workers.ggevent.GeventPyWSGIWorker", + "gevent_pywsgi": "gunicorn.workers.ggevent.GeventPyWSGIWorker", + "tornado": "gunicorn.workers.gtornado.TornadoWorker", + "gthread": "gunicorn.workers.gthread.ThreadWorker", +} diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/workers/base.py b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/base.py new file mode 100644 index 0000000..93c465c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/base.py @@ -0,0 +1,287 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import io +import os +import signal +import sys +import time +import traceback +from datetime import datetime +from random import randint +from ssl import SSLError + +from gunicorn import util +from gunicorn.http.errors import ( + ForbiddenProxyRequest, InvalidHeader, + InvalidHeaderName, InvalidHTTPVersion, + InvalidProxyLine, InvalidRequestLine, + InvalidRequestMethod, InvalidSchemeHeaders, + LimitRequestHeaders, LimitRequestLine, + UnsupportedTransferCoding, + ConfigurationProblem, ObsoleteFolding, +) +from gunicorn.http.wsgi import Response, default_environ +from gunicorn.reloader import reloader_engines +from gunicorn.workers.workertmp import WorkerTmp + + +class Worker: + + SIGNALS = [getattr(signal, "SIG%s" % x) for x in ( + "ABRT HUP QUIT INT TERM USR1 USR2 WINCH CHLD".split() + )] + + PIPE = [] + + def __init__(self, age, ppid, sockets, app, timeout, cfg, log): + """\ + This is called pre-fork so it shouldn't do anything to the + current process. If there's a need to make process wide + changes you'll want to do that in ``self.init_process()``. + """ + self.age = age + self.pid = "[booting]" + self.ppid = ppid + self.sockets = sockets + self.app = app + self.timeout = timeout + self.cfg = cfg + self.booted = False + self.aborted = False + self.reloader = None + + self.nr = 0 + + if cfg.max_requests > 0: + jitter = randint(0, cfg.max_requests_jitter) + self.max_requests = cfg.max_requests + jitter + else: + self.max_requests = sys.maxsize + + self.alive = True + self.log = log + self.tmp = WorkerTmp(cfg) + + def __str__(self): + return "" % self.pid + + def notify(self): + """\ + Your worker subclass must arrange to have this method called + once every ``self.timeout`` seconds. If you fail in accomplishing + this task, the master process will murder your workers. + """ + self.tmp.notify() + + def run(self): + """\ + This is the mainloop of a worker process. You should override + this method in a subclass to provide the intended behaviour + for your particular evil schemes. + """ + raise NotImplementedError() + + def init_process(self): + """\ + If you override this method in a subclass, the last statement + in the function should be to call this method with + super().init_process() so that the ``run()`` loop is initiated. + """ + + # set environment' variables + if self.cfg.env: + for k, v in self.cfg.env.items(): + os.environ[k] = v + + util.set_owner_process(self.cfg.uid, self.cfg.gid, + initgroups=self.cfg.initgroups) + + # Reseed the random number generator + util.seed() + + # For waking ourselves up + self.PIPE = os.pipe() + for p in self.PIPE: + util.set_non_blocking(p) + util.close_on_exec(p) + + # Prevent fd inheritance + for s in self.sockets: + util.close_on_exec(s) + util.close_on_exec(self.tmp.fileno()) + + self.wait_fds = self.sockets + [self.PIPE[0]] + + self.log.close_on_exec() + + self.init_signals() + + # start the reloader + if self.cfg.reload: + def changed(fname): + self.log.info("Worker reloading: %s modified", fname) + self.alive = False + os.write(self.PIPE[1], b"1") + self.cfg.worker_int(self) + time.sleep(0.1) + sys.exit(0) + + reloader_cls = reloader_engines[self.cfg.reload_engine] + self.reloader = reloader_cls(extra_files=self.cfg.reload_extra_files, + callback=changed) + + self.load_wsgi() + if self.reloader: + self.reloader.start() + + self.cfg.post_worker_init(self) + + # Enter main run loop + self.booted = True + self.run() + + def load_wsgi(self): + try: + self.wsgi = self.app.wsgi() + except SyntaxError as e: + if not self.cfg.reload: + raise + + self.log.exception(e) + + # fix from PR #1228 + # storing the traceback into exc_tb will create a circular reference. + # per https://docs.python.org/2/library/sys.html#sys.exc_info warning, + # delete the traceback after use. + try: + _, exc_val, exc_tb = sys.exc_info() + self.reloader.add_extra_file(exc_val.filename) + + tb_string = io.StringIO() + traceback.print_tb(exc_tb, file=tb_string) + self.wsgi = util.make_fail_app(tb_string.getvalue()) + finally: + del exc_tb + + def init_signals(self): + # reset signaling + for s in self.SIGNALS: + signal.signal(s, signal.SIG_DFL) + # init new signaling + signal.signal(signal.SIGQUIT, self.handle_quit) + signal.signal(signal.SIGTERM, self.handle_exit) + signal.signal(signal.SIGINT, self.handle_quit) + signal.signal(signal.SIGWINCH, self.handle_winch) + signal.signal(signal.SIGUSR1, self.handle_usr1) + signal.signal(signal.SIGABRT, self.handle_abort) + + # Don't let SIGTERM and SIGUSR1 disturb active requests + # by interrupting system calls + signal.siginterrupt(signal.SIGTERM, False) + signal.siginterrupt(signal.SIGUSR1, False) + + if hasattr(signal, 'set_wakeup_fd'): + signal.set_wakeup_fd(self.PIPE[1]) + + def handle_usr1(self, sig, frame): + self.log.reopen_files() + + def handle_exit(self, sig, frame): + self.alive = False + + def handle_quit(self, sig, frame): + self.alive = False + # worker_int callback + self.cfg.worker_int(self) + time.sleep(0.1) + sys.exit(0) + + def handle_abort(self, sig, frame): + self.alive = False + self.cfg.worker_abort(self) + sys.exit(1) + + def handle_error(self, req, client, addr, exc): + request_start = datetime.now() + addr = addr or ('', -1) # unix socket case + if isinstance(exc, ( + InvalidRequestLine, InvalidRequestMethod, + InvalidHTTPVersion, InvalidHeader, InvalidHeaderName, + LimitRequestLine, LimitRequestHeaders, + InvalidProxyLine, ForbiddenProxyRequest, + InvalidSchemeHeaders, UnsupportedTransferCoding, + ConfigurationProblem, ObsoleteFolding, + SSLError, + )): + + status_int = 400 + reason = "Bad Request" + + if isinstance(exc, InvalidRequestLine): + mesg = "Invalid Request Line '%s'" % str(exc) + elif isinstance(exc, InvalidRequestMethod): + mesg = "Invalid Method '%s'" % str(exc) + elif isinstance(exc, InvalidHTTPVersion): + mesg = "Invalid HTTP Version '%s'" % str(exc) + elif isinstance(exc, UnsupportedTransferCoding): + mesg = "%s" % str(exc) + status_int = 501 + elif isinstance(exc, ConfigurationProblem): + mesg = "%s" % str(exc) + status_int = 500 + elif isinstance(exc, ObsoleteFolding): + mesg = "%s" % str(exc) + elif isinstance(exc, (InvalidHeaderName, InvalidHeader,)): + mesg = "%s" % str(exc) + if not req and hasattr(exc, "req"): + req = exc.req # for access log + elif isinstance(exc, LimitRequestLine): + mesg = "%s" % str(exc) + elif isinstance(exc, LimitRequestHeaders): + reason = "Request Header Fields Too Large" + mesg = "Error parsing headers: '%s'" % str(exc) + status_int = 431 + elif isinstance(exc, InvalidProxyLine): + mesg = "'%s'" % str(exc) + elif isinstance(exc, ForbiddenProxyRequest): + reason = "Forbidden" + mesg = "Request forbidden" + status_int = 403 + elif isinstance(exc, InvalidSchemeHeaders): + mesg = "%s" % str(exc) + elif isinstance(exc, SSLError): + reason = "Forbidden" + mesg = "'%s'" % str(exc) + status_int = 403 + + msg = "Invalid request from ip={ip}: {error}" + self.log.warning(msg.format(ip=addr[0], error=str(exc))) + else: + if hasattr(req, "uri"): + self.log.exception("Error handling request %s", req.uri) + else: + self.log.exception("Error handling request (no URI read)") + status_int = 500 + reason = "Internal Server Error" + mesg = "" + + if req is not None: + request_time = datetime.now() - request_start + environ = default_environ(req, client, self.cfg) + environ['REMOTE_ADDR'] = addr[0] + environ['REMOTE_PORT'] = str(addr[1]) + resp = Response(req, client, self.cfg) + resp.status = "%s %s" % (status_int, reason) + resp.response_length = len(mesg) + self.log.access(resp, req, environ, request_time) + + try: + util.write_error(client, status_int, reason, mesg) + except Exception: + self.log.debug("Failed to send error message.") + + def handle_winch(self, sig, fname): + # Ignore SIGWINCH in worker. Fixes a crash on OpenBSD. + self.log.debug("worker: SIGWINCH ignored.") diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/workers/base_async.py b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/base_async.py new file mode 100644 index 0000000..9466d6a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/base_async.py @@ -0,0 +1,147 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +from datetime import datetime +import errno +import socket +import ssl +import sys + +from gunicorn import http +from gunicorn.http import wsgi +from gunicorn import util +from gunicorn.workers import base + +ALREADY_HANDLED = object() + + +class AsyncWorker(base.Worker): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.worker_connections = self.cfg.worker_connections + + def timeout_ctx(self): + raise NotImplementedError() + + def is_already_handled(self, respiter): + # some workers will need to overload this function to raise a StopIteration + return respiter == ALREADY_HANDLED + + def handle(self, listener, client, addr): + req = None + try: + parser = http.RequestParser(self.cfg, client, addr) + try: + listener_name = listener.getsockname() + if not self.cfg.keepalive: + req = next(parser) + self.handle_request(listener_name, req, client, addr) + else: + # keepalive loop + proxy_protocol_info = {} + while True: + req = None + with self.timeout_ctx(): + req = next(parser) + if not req: + break + if req.proxy_protocol_info: + proxy_protocol_info = req.proxy_protocol_info + else: + req.proxy_protocol_info = proxy_protocol_info + self.handle_request(listener_name, req, client, addr) + except http.errors.NoMoreData as e: + self.log.debug("Ignored premature client disconnection. %s", e) + except StopIteration as e: + self.log.debug("Closing connection. %s", e) + except ssl.SSLError: + # pass to next try-except level + util.reraise(*sys.exc_info()) + except OSError: + # pass to next try-except level + util.reraise(*sys.exc_info()) + except Exception as e: + self.handle_error(req, client, addr, e) + except ssl.SSLError as e: + if e.args[0] == ssl.SSL_ERROR_EOF: + self.log.debug("ssl connection closed") + client.close() + else: + self.log.debug("Error processing SSL request.") + self.handle_error(req, client, addr, e) + except OSError as e: + if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN): + self.log.exception("Socket error processing request.") + else: + if e.errno == errno.ECONNRESET: + self.log.debug("Ignoring connection reset") + elif e.errno == errno.ENOTCONN: + self.log.debug("Ignoring socket not connected") + else: + self.log.debug("Ignoring EPIPE") + except BaseException as e: + self.handle_error(req, client, addr, e) + finally: + util.close(client) + + def handle_request(self, listener_name, req, sock, addr): + request_start = datetime.now() + environ = {} + resp = None + try: + self.cfg.pre_request(self, req) + resp, environ = wsgi.create(req, sock, addr, + listener_name, self.cfg) + environ["wsgi.multithread"] = True + self.nr += 1 + if self.nr >= self.max_requests: + if self.alive: + self.log.info("Autorestarting worker after current request.") + self.alive = False + + if not self.alive or not self.cfg.keepalive: + resp.force_close() + + respiter = self.wsgi(environ, resp.start_response) + if self.is_already_handled(respiter): + return False + try: + if isinstance(respiter, environ['wsgi.file_wrapper']): + resp.write_file(respiter) + else: + for item in respiter: + resp.write(item) + resp.close() + finally: + request_time = datetime.now() - request_start + self.log.access(resp, req, environ, request_time) + if hasattr(respiter, "close"): + respiter.close() + if resp.should_close(): + raise StopIteration() + except StopIteration: + raise + except OSError: + # If the original exception was a socket.error we delegate + # handling it to the caller (where handle() might ignore it) + util.reraise(*sys.exc_info()) + except Exception: + if resp and resp.headers_sent: + # If the requests have already been sent, we should close the + # connection to indicate the error. + self.log.exception("Error handling request") + try: + sock.shutdown(socket.SHUT_RDWR) + sock.close() + except OSError: + pass + raise StopIteration() + raise + finally: + try: + self.cfg.post_request(self, req, environ, resp) + except Exception: + self.log.exception("Exception in post_request hook") + return True diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/workers/geventlet.py b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/geventlet.py new file mode 100644 index 0000000..087eb61 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/geventlet.py @@ -0,0 +1,186 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +from functools import partial +import sys + +try: + import eventlet +except ImportError: + raise RuntimeError("eventlet worker requires eventlet 0.24.1 or higher") +else: + from packaging.version import parse as parse_version + if parse_version(eventlet.__version__) < parse_version('0.24.1'): + raise RuntimeError("eventlet worker requires eventlet 0.24.1 or higher") + +from eventlet import hubs, greenthread +from eventlet.greenio import GreenSocket +import eventlet.wsgi +import greenlet + +from gunicorn.workers.base_async import AsyncWorker +from gunicorn.sock import ssl_wrap_socket + +# ALREADY_HANDLED is removed in 0.30.3+ now it's `WSGI_LOCAL.already_handled: bool` +# https://github.com/eventlet/eventlet/pull/544 +EVENTLET_WSGI_LOCAL = getattr(eventlet.wsgi, "WSGI_LOCAL", None) +EVENTLET_ALREADY_HANDLED = getattr(eventlet.wsgi, "ALREADY_HANDLED", None) + + +def _eventlet_socket_sendfile(self, file, offset=0, count=None): + # Based on the implementation in gevent which in turn is slightly + # modified from the standard library implementation. + if self.gettimeout() == 0: + raise ValueError("non-blocking sockets are not supported") + if offset: + file.seek(offset) + blocksize = min(count, 8192) if count else 8192 + total_sent = 0 + # localize variable access to minimize overhead + file_read = file.read + sock_send = self.send + try: + while True: + if count: + blocksize = min(count - total_sent, blocksize) + if blocksize <= 0: + break + data = memoryview(file_read(blocksize)) + if not data: + break # EOF + while True: + try: + sent = sock_send(data) + except BlockingIOError: + continue + else: + total_sent += sent + if sent < len(data): + data = data[sent:] + else: + break + return total_sent + finally: + if total_sent > 0 and hasattr(file, 'seek'): + file.seek(offset + total_sent) + + +def _eventlet_serve(sock, handle, concurrency): + """ + Serve requests forever. + + This code is nearly identical to ``eventlet.convenience.serve`` except + that it attempts to join the pool at the end, which allows for gunicorn + graceful shutdowns. + """ + pool = eventlet.greenpool.GreenPool(concurrency) + server_gt = eventlet.greenthread.getcurrent() + + while True: + try: + conn, addr = sock.accept() + gt = pool.spawn(handle, conn, addr) + gt.link(_eventlet_stop, server_gt, conn) + conn, addr, gt = None, None, None + except eventlet.StopServe: + sock.close() + pool.waitall() + return + + +def _eventlet_stop(client, server, conn): + """ + Stop a greenlet handling a request and close its connection. + + This code is lifted from eventlet so as not to depend on undocumented + functions in the library. + """ + try: + try: + client.wait() + finally: + conn.close() + except greenlet.GreenletExit: + pass + except Exception: + greenthread.kill(server, *sys.exc_info()) + + +def patch_sendfile(): + # As of eventlet 0.25.1, GreenSocket.sendfile doesn't exist, + # meaning the native implementations of socket.sendfile will be used. + # If os.sendfile exists, it will attempt to use that, failing explicitly + # if the socket is in non-blocking mode, which the underlying + # socket object /is/. Even the regular _sendfile_use_send will + # fail in that way; plus, it would use the underlying socket.send which isn't + # properly cooperative. So we have to monkey-patch a working socket.sendfile() + # into GreenSocket; in this method, `self.send` will be the GreenSocket's + # send method which is properly cooperative. + if not hasattr(GreenSocket, 'sendfile'): + GreenSocket.sendfile = _eventlet_socket_sendfile + + +class EventletWorker(AsyncWorker): + + def patch(self): + hubs.use_hub() + eventlet.monkey_patch() + patch_sendfile() + + def is_already_handled(self, respiter): + # eventlet >= 0.30.3 + if getattr(EVENTLET_WSGI_LOCAL, "already_handled", None): + raise StopIteration() + # eventlet < 0.30.3 + if respiter == EVENTLET_ALREADY_HANDLED: + raise StopIteration() + return super().is_already_handled(respiter) + + def init_process(self): + self.patch() + super().init_process() + + def handle_quit(self, sig, frame): + eventlet.spawn(super().handle_quit, sig, frame) + + def handle_usr1(self, sig, frame): + eventlet.spawn(super().handle_usr1, sig, frame) + + def timeout_ctx(self): + return eventlet.Timeout(self.cfg.keepalive or None, False) + + def handle(self, listener, client, addr): + if self.cfg.is_ssl: + client = ssl_wrap_socket(client, self.cfg) + super().handle(listener, client, addr) + + def run(self): + acceptors = [] + for sock in self.sockets: + gsock = GreenSocket(sock) + gsock.setblocking(1) + hfun = partial(self.handle, gsock) + acceptor = eventlet.spawn(_eventlet_serve, gsock, hfun, + self.worker_connections) + + acceptors.append(acceptor) + eventlet.sleep(0.0) + + while self.alive: + self.notify() + eventlet.sleep(1.0) + + self.notify() + t = None + try: + with eventlet.Timeout(self.cfg.graceful_timeout) as t: + for a in acceptors: + a.kill(eventlet.StopServe()) + for a in acceptors: + a.wait() + except eventlet.Timeout as te: + if te != t: + raise + for a in acceptors: + a.kill() diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/workers/ggevent.py b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/ggevent.py new file mode 100644 index 0000000..b9b9b44 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/ggevent.py @@ -0,0 +1,193 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import os +import sys +from datetime import datetime +from functools import partial +import time + +try: + import gevent +except ImportError: + raise RuntimeError("gevent worker requires gevent 1.4 or higher") +else: + from packaging.version import parse as parse_version + if parse_version(gevent.__version__) < parse_version('1.4'): + raise RuntimeError("gevent worker requires gevent 1.4 or higher") + +from gevent.pool import Pool +from gevent.server import StreamServer +from gevent import hub, monkey, socket, pywsgi + +import gunicorn +from gunicorn.http.wsgi import base_environ +from gunicorn.sock import ssl_context +from gunicorn.workers.base_async import AsyncWorker + +VERSION = "gevent/%s gunicorn/%s" % (gevent.__version__, gunicorn.__version__) + + +class GeventWorker(AsyncWorker): + + server_class = None + wsgi_handler = None + + def patch(self): + monkey.patch_all() + + # patch sockets + sockets = [] + for s in self.sockets: + sockets.append(socket.socket(s.FAMILY, socket.SOCK_STREAM, + fileno=s.sock.fileno())) + self.sockets = sockets + + def notify(self): + super().notify() + if self.ppid != os.getppid(): + self.log.info("Parent changed, shutting down: %s", self) + sys.exit(0) + + def timeout_ctx(self): + return gevent.Timeout(self.cfg.keepalive, False) + + def run(self): + servers = [] + ssl_args = {} + + if self.cfg.is_ssl: + ssl_args = {"ssl_context": ssl_context(self.cfg)} + + for s in self.sockets: + s.setblocking(1) + pool = Pool(self.worker_connections) + if self.server_class is not None: + environ = base_environ(self.cfg) + environ.update({ + "wsgi.multithread": True, + "SERVER_SOFTWARE": VERSION, + }) + server = self.server_class( + s, application=self.wsgi, spawn=pool, log=self.log, + handler_class=self.wsgi_handler, environ=environ, + **ssl_args) + else: + hfun = partial(self.handle, s) + server = StreamServer(s, handle=hfun, spawn=pool, **ssl_args) + if self.cfg.workers > 1: + server.max_accept = 1 + + server.start() + servers.append(server) + + while self.alive: + self.notify() + gevent.sleep(1.0) + + try: + # Stop accepting requests + for server in servers: + if hasattr(server, 'close'): # gevent 1.0 + server.close() + if hasattr(server, 'kill'): # gevent < 1.0 + server.kill() + + # Handle current requests until graceful_timeout + ts = time.time() + while time.time() - ts <= self.cfg.graceful_timeout: + accepting = 0 + for server in servers: + if server.pool.free_count() != server.pool.size: + accepting += 1 + + # if no server is accepting a connection, we can exit + if not accepting: + return + + self.notify() + gevent.sleep(1.0) + + # Force kill all active the handlers + self.log.warning("Worker graceful timeout (pid:%s)", self.pid) + for server in servers: + server.stop(timeout=1) + except Exception: + pass + + def handle(self, listener, client, addr): + # Connected socket timeout defaults to socket.getdefaulttimeout(). + # This forces to blocking mode. + client.setblocking(1) + super().handle(listener, client, addr) + + def handle_request(self, listener_name, req, sock, addr): + try: + super().handle_request(listener_name, req, sock, addr) + except gevent.GreenletExit: + pass + except SystemExit: + pass + + def handle_quit(self, sig, frame): + # Move this out of the signal handler so we can use + # blocking calls. See #1126 + gevent.spawn(super().handle_quit, sig, frame) + + def handle_usr1(self, sig, frame): + # Make the gevent workers handle the usr1 signal + # by deferring to a new greenlet. See #1645 + gevent.spawn(super().handle_usr1, sig, frame) + + def init_process(self): + self.patch() + hub.reinit() + super().init_process() + + +class GeventResponse: + + status = None + headers = None + sent = None + + def __init__(self, status, headers, clength): + self.status = status + self.headers = headers + self.sent = clength + + +class PyWSGIHandler(pywsgi.WSGIHandler): + + def log_request(self): + start = datetime.fromtimestamp(self.time_start) + finish = datetime.fromtimestamp(self.time_finish) + response_time = finish - start + resp_headers = getattr(self, 'response_headers', {}) + + # Status is expected to be a string but is encoded to bytes in gevent for PY3 + # Except when it isn't because gevent uses hardcoded strings for network errors. + status = self.status.decode() if isinstance(self.status, bytes) else self.status + resp = GeventResponse(status, resp_headers, self.response_length) + if hasattr(self, 'headers'): + req_headers = self.headers.items() + else: + req_headers = [] + self.server.log.access(resp, req_headers, self.environ, response_time) + + def get_environ(self): + env = super().get_environ() + env['gunicorn.sock'] = self.socket + env['RAW_URI'] = self.path + return env + + +class PyWSGIServer(pywsgi.WSGIServer): + pass + + +class GeventPyWSGIWorker(GeventWorker): + "The Gevent StreamServer based workers." + server_class = PyWSGIServer + wsgi_handler = PyWSGIHandler diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/workers/gthread.py b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/gthread.py new file mode 100644 index 0000000..7a23228 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/gthread.py @@ -0,0 +1,372 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +# design: +# A threaded worker accepts connections in the main loop, accepted +# connections are added to the thread pool as a connection job. +# Keepalive connections are put back in the loop waiting for an event. +# If no event happen after the keep alive timeout, the connection is +# closed. +# pylint: disable=no-else-break + +from concurrent import futures +import errno +import os +import selectors +import socket +import ssl +import sys +import time +from collections import deque +from datetime import datetime +from functools import partial +from threading import RLock + +from . import base +from .. import http +from .. import util +from .. import sock +from ..http import wsgi + + +class TConn: + + def __init__(self, cfg, sock, client, server): + self.cfg = cfg + self.sock = sock + self.client = client + self.server = server + + self.timeout = None + self.parser = None + self.initialized = False + + # set the socket to non blocking + self.sock.setblocking(False) + + def init(self): + self.initialized = True + self.sock.setblocking(True) + + if self.parser is None: + # wrap the socket if needed + if self.cfg.is_ssl: + self.sock = sock.ssl_wrap_socket(self.sock, self.cfg) + + # initialize the parser + self.parser = http.RequestParser(self.cfg, self.sock, self.client) + + def set_timeout(self): + # set the timeout + self.timeout = time.time() + self.cfg.keepalive + + def close(self): + util.close(self.sock) + + +class ThreadWorker(base.Worker): + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.worker_connections = self.cfg.worker_connections + self.max_keepalived = self.cfg.worker_connections - self.cfg.threads + # initialise the pool + self.tpool = None + self.poller = None + self._lock = None + self.futures = deque() + self._keep = deque() + self.nr_conns = 0 + + @classmethod + def check_config(cls, cfg, log): + max_keepalived = cfg.worker_connections - cfg.threads + + if max_keepalived <= 0 and cfg.keepalive: + log.warning("No keepalived connections can be handled. " + + "Check the number of worker connections and threads.") + + def init_process(self): + self.tpool = self.get_thread_pool() + self.poller = selectors.DefaultSelector() + self._lock = RLock() + super().init_process() + + def get_thread_pool(self): + """Override this method to customize how the thread pool is created""" + return futures.ThreadPoolExecutor(max_workers=self.cfg.threads) + + def handle_quit(self, sig, frame): + self.alive = False + # worker_int callback + self.cfg.worker_int(self) + self.tpool.shutdown(False) + time.sleep(0.1) + sys.exit(0) + + def _wrap_future(self, fs, conn): + fs.conn = conn + self.futures.append(fs) + fs.add_done_callback(self.finish_request) + + def enqueue_req(self, conn): + conn.init() + # submit the connection to a worker + fs = self.tpool.submit(self.handle, conn) + self._wrap_future(fs, conn) + + def accept(self, server, listener): + try: + sock, client = listener.accept() + # initialize the connection object + conn = TConn(self.cfg, sock, client, server) + + self.nr_conns += 1 + # wait until socket is readable + with self._lock: + self.poller.register(conn.sock, selectors.EVENT_READ, + partial(self.on_client_socket_readable, conn)) + except OSError as e: + if e.errno not in (errno.EAGAIN, errno.ECONNABORTED, + errno.EWOULDBLOCK): + raise + + def on_client_socket_readable(self, conn, client): + with self._lock: + # unregister the client from the poller + self.poller.unregister(client) + + if conn.initialized: + # remove the connection from keepalive + try: + self._keep.remove(conn) + except ValueError: + # race condition + return + + # submit the connection to a worker + self.enqueue_req(conn) + + def murder_keepalived(self): + now = time.time() + while True: + with self._lock: + try: + # remove the connection from the queue + conn = self._keep.popleft() + except IndexError: + break + + delta = conn.timeout - now + if delta > 0: + # add the connection back to the queue + with self._lock: + self._keep.appendleft(conn) + break + else: + self.nr_conns -= 1 + # remove the socket from the poller + with self._lock: + try: + self.poller.unregister(conn.sock) + except OSError as e: + if e.errno != errno.EBADF: + raise + except KeyError: + # already removed by the system, continue + pass + except ValueError: + # already removed by the system continue + pass + + # close the socket + conn.close() + + def is_parent_alive(self): + # If our parent changed then we shut down. + if self.ppid != os.getppid(): + self.log.info("Parent changed, shutting down: %s", self) + return False + return True + + def run(self): + # init listeners, add them to the event loop + for sock in self.sockets: + sock.setblocking(False) + # a race condition during graceful shutdown may make the listener + # name unavailable in the request handler so capture it once here + server = sock.getsockname() + acceptor = partial(self.accept, server) + self.poller.register(sock, selectors.EVENT_READ, acceptor) + + while self.alive: + # notify the arbiter we are alive + self.notify() + + # can we accept more connections? + if self.nr_conns < self.worker_connections: + # wait for an event + events = self.poller.select(1.0) + for key, _ in events: + callback = key.data + callback(key.fileobj) + + # check (but do not wait) for finished requests + result = futures.wait(self.futures, timeout=0, + return_when=futures.FIRST_COMPLETED) + else: + # wait for a request to finish + result = futures.wait(self.futures, timeout=1.0, + return_when=futures.FIRST_COMPLETED) + + # clean up finished requests + for fut in result.done: + self.futures.remove(fut) + + if not self.is_parent_alive(): + break + + # handle keepalive timeouts + self.murder_keepalived() + + self.tpool.shutdown(False) + self.poller.close() + + for s in self.sockets: + s.close() + + futures.wait(self.futures, timeout=self.cfg.graceful_timeout) + + def finish_request(self, fs): + if fs.cancelled(): + self.nr_conns -= 1 + fs.conn.close() + return + + try: + (keepalive, conn) = fs.result() + # if the connection should be kept alived add it + # to the eventloop and record it + if keepalive and self.alive: + # flag the socket as non blocked + conn.sock.setblocking(False) + + # register the connection + conn.set_timeout() + with self._lock: + self._keep.append(conn) + + # add the socket to the event loop + self.poller.register(conn.sock, selectors.EVENT_READ, + partial(self.on_client_socket_readable, conn)) + else: + self.nr_conns -= 1 + conn.close() + except Exception: + # an exception happened, make sure to close the + # socket. + self.nr_conns -= 1 + fs.conn.close() + + def handle(self, conn): + keepalive = False + req = None + try: + req = next(conn.parser) + if not req: + return (False, conn) + + # handle the request + keepalive = self.handle_request(req, conn) + if keepalive: + return (keepalive, conn) + except http.errors.NoMoreData as e: + self.log.debug("Ignored premature client disconnection. %s", e) + + except StopIteration as e: + self.log.debug("Closing connection. %s", e) + except ssl.SSLError as e: + if e.args[0] == ssl.SSL_ERROR_EOF: + self.log.debug("ssl connection closed") + conn.sock.close() + else: + self.log.debug("Error processing SSL request.") + self.handle_error(req, conn.sock, conn.client, e) + + except OSError as e: + if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN): + self.log.exception("Socket error processing request.") + else: + if e.errno == errno.ECONNRESET: + self.log.debug("Ignoring connection reset") + elif e.errno == errno.ENOTCONN: + self.log.debug("Ignoring socket not connected") + else: + self.log.debug("Ignoring connection epipe") + except Exception as e: + self.handle_error(req, conn.sock, conn.client, e) + + return (False, conn) + + def handle_request(self, req, conn): + environ = {} + resp = None + try: + self.cfg.pre_request(self, req) + request_start = datetime.now() + resp, environ = wsgi.create(req, conn.sock, conn.client, + conn.server, self.cfg) + environ["wsgi.multithread"] = True + self.nr += 1 + if self.nr >= self.max_requests: + if self.alive: + self.log.info("Autorestarting worker after current request.") + self.alive = False + resp.force_close() + + if not self.alive or not self.cfg.keepalive: + resp.force_close() + elif len(self._keep) >= self.max_keepalived: + resp.force_close() + + respiter = self.wsgi(environ, resp.start_response) + try: + if isinstance(respiter, environ['wsgi.file_wrapper']): + resp.write_file(respiter) + else: + for item in respiter: + resp.write(item) + + resp.close() + finally: + request_time = datetime.now() - request_start + self.log.access(resp, req, environ, request_time) + if hasattr(respiter, "close"): + respiter.close() + + if resp.should_close(): + self.log.debug("Closing connection.") + return False + except OSError: + # pass to next try-except level + util.reraise(*sys.exc_info()) + except Exception: + if resp and resp.headers_sent: + # If the requests have already been sent, we should close the + # connection to indicate the error. + self.log.exception("Error handling request") + try: + conn.sock.shutdown(socket.SHUT_RDWR) + conn.sock.close() + except OSError: + pass + raise StopIteration() + raise + finally: + try: + self.cfg.post_request(self, req, environ, resp) + except Exception: + self.log.exception("Exception in post_request hook") + + return True diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/workers/gtornado.py b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/gtornado.py new file mode 100644 index 0000000..544af7d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/gtornado.py @@ -0,0 +1,166 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import os +import sys + +try: + import tornado +except ImportError: + raise RuntimeError("You need tornado installed to use this worker.") +import tornado.web +import tornado.httpserver +from tornado.ioloop import IOLoop, PeriodicCallback +from tornado.wsgi import WSGIContainer +from gunicorn.workers.base import Worker +from gunicorn import __version__ as gversion +from gunicorn.sock import ssl_context + + +# Tornado 5.0 updated its IOLoop, and the `io_loop` arguments to many +# Tornado functions have been removed in Tornado 5.0. Also, they no +# longer store PeriodCallbacks in ioloop._callbacks. Instead we store +# them on our side, and use stop() on them when stopping the worker. +# See https://www.tornadoweb.org/en/stable/releases/v5.0.0.html#backwards-compatibility-notes +# for more details. +TORNADO5 = tornado.version_info >= (5, 0, 0) + + +class TornadoWorker(Worker): + + @classmethod + def setup(cls): + web = sys.modules.pop("tornado.web") + old_clear = web.RequestHandler.clear + + def clear(self): + old_clear(self) + if "Gunicorn" not in self._headers["Server"]: + self._headers["Server"] += " (Gunicorn/%s)" % gversion + web.RequestHandler.clear = clear + sys.modules["tornado.web"] = web + + def handle_exit(self, sig, frame): + if self.alive: + super().handle_exit(sig, frame) + + def handle_request(self): + self.nr += 1 + if self.alive and self.nr >= self.max_requests: + self.log.info("Autorestarting worker after current request.") + self.alive = False + + def watchdog(self): + if self.alive: + self.notify() + + if self.ppid != os.getppid(): + self.log.info("Parent changed, shutting down: %s", self) + self.alive = False + + def heartbeat(self): + if not self.alive: + if self.server_alive: + if hasattr(self, 'server'): + try: + self.server.stop() + except Exception: + pass + self.server_alive = False + else: + if TORNADO5: + for callback in self.callbacks: + callback.stop() + self.ioloop.stop() + else: + if not self.ioloop._callbacks: + self.ioloop.stop() + + def init_process(self): + # IOLoop cannot survive a fork or be shared across processes + # in any way. When multiple processes are being used, each process + # should create its own IOLoop. We should clear current IOLoop + # if exists before os.fork. + IOLoop.clear_current() + super().init_process() + + def run(self): + self.ioloop = IOLoop.instance() + self.alive = True + self.server_alive = False + + if TORNADO5: + self.callbacks = [] + self.callbacks.append(PeriodicCallback(self.watchdog, 1000)) + self.callbacks.append(PeriodicCallback(self.heartbeat, 1000)) + for callback in self.callbacks: + callback.start() + else: + PeriodicCallback(self.watchdog, 1000, io_loop=self.ioloop).start() + PeriodicCallback(self.heartbeat, 1000, io_loop=self.ioloop).start() + + # Assume the app is a WSGI callable if its not an + # instance of tornado.web.Application or is an + # instance of tornado.wsgi.WSGIApplication + app = self.wsgi + + if tornado.version_info[0] < 6: + if not isinstance(app, tornado.web.Application) or \ + isinstance(app, tornado.wsgi.WSGIApplication): + app = WSGIContainer(app) + elif not isinstance(app, WSGIContainer) and \ + not isinstance(app, tornado.web.Application): + app = WSGIContainer(app) + + # Monkey-patching HTTPConnection.finish to count the + # number of requests being handled by Tornado. This + # will help gunicorn shutdown the worker if max_requests + # is exceeded. + httpserver = sys.modules["tornado.httpserver"] + if hasattr(httpserver, 'HTTPConnection'): + old_connection_finish = httpserver.HTTPConnection.finish + + def finish(other): + self.handle_request() + old_connection_finish(other) + httpserver.HTTPConnection.finish = finish + sys.modules["tornado.httpserver"] = httpserver + + server_class = tornado.httpserver.HTTPServer + else: + + class _HTTPServer(tornado.httpserver.HTTPServer): + + def on_close(instance, server_conn): + self.handle_request() + super().on_close(server_conn) + + server_class = _HTTPServer + + if self.cfg.is_ssl: + if TORNADO5: + server = server_class(app, ssl_options=ssl_context(self.cfg)) + else: + server = server_class(app, io_loop=self.ioloop, + ssl_options=ssl_context(self.cfg)) + else: + if TORNADO5: + server = server_class(app) + else: + server = server_class(app, io_loop=self.ioloop) + + self.server = server + self.server_alive = True + + for s in self.sockets: + s.setblocking(0) + if hasattr(server, "add_socket"): # tornado > 2.0 + server.add_socket(s) + elif hasattr(server, "_sockets"): # tornado 2.0 + server._sockets[s.fileno()] = s + + server.no_keep_alive = self.cfg.keepalive <= 0 + server.start(num_processes=1) + + self.ioloop.start() diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/workers/sync.py b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/sync.py new file mode 100644 index 0000000..4c029f9 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/sync.py @@ -0,0 +1,209 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. +# + +from datetime import datetime +import errno +import os +import select +import socket +import ssl +import sys + +from gunicorn import http +from gunicorn.http import wsgi +from gunicorn import sock +from gunicorn import util +from gunicorn.workers import base + + +class StopWaiting(Exception): + """ exception raised to stop waiting for a connection """ + + +class SyncWorker(base.Worker): + + def accept(self, listener): + client, addr = listener.accept() + client.setblocking(1) + util.close_on_exec(client) + self.handle(listener, client, addr) + + def wait(self, timeout): + try: + self.notify() + ret = select.select(self.wait_fds, [], [], timeout) + if ret[0]: + if self.PIPE[0] in ret[0]: + os.read(self.PIPE[0], 1) + return ret[0] + + except OSError as e: + if e.args[0] == errno.EINTR: + return self.sockets + if e.args[0] == errno.EBADF: + if self.nr < 0: + return self.sockets + else: + raise StopWaiting + raise + + def is_parent_alive(self): + # If our parent changed then we shut down. + if self.ppid != os.getppid(): + self.log.info("Parent changed, shutting down: %s", self) + return False + return True + + def run_for_one(self, timeout): + listener = self.sockets[0] + while self.alive: + self.notify() + + # Accept a connection. If we get an error telling us + # that no connection is waiting we fall down to the + # select which is where we'll wait for a bit for new + # workers to come give us some love. + try: + self.accept(listener) + # Keep processing clients until no one is waiting. This + # prevents the need to select() for every client that we + # process. + continue + + except OSError as e: + if e.errno not in (errno.EAGAIN, errno.ECONNABORTED, + errno.EWOULDBLOCK): + raise + + if not self.is_parent_alive(): + return + + try: + self.wait(timeout) + except StopWaiting: + return + + def run_for_multiple(self, timeout): + while self.alive: + self.notify() + + try: + ready = self.wait(timeout) + except StopWaiting: + return + + if ready is not None: + for listener in ready: + if listener == self.PIPE[0]: + continue + + try: + self.accept(listener) + except OSError as e: + if e.errno not in (errno.EAGAIN, errno.ECONNABORTED, + errno.EWOULDBLOCK): + raise + + if not self.is_parent_alive(): + return + + def run(self): + # if no timeout is given the worker will never wait and will + # use the CPU for nothing. This minimal timeout prevent it. + timeout = self.timeout or 0.5 + + # self.socket appears to lose its blocking status after + # we fork in the arbiter. Reset it here. + for s in self.sockets: + s.setblocking(0) + + if len(self.sockets) > 1: + self.run_for_multiple(timeout) + else: + self.run_for_one(timeout) + + def handle(self, listener, client, addr): + req = None + try: + if self.cfg.is_ssl: + client = sock.ssl_wrap_socket(client, self.cfg) + parser = http.RequestParser(self.cfg, client, addr) + req = next(parser) + self.handle_request(listener, req, client, addr) + except http.errors.NoMoreData as e: + self.log.debug("Ignored premature client disconnection. %s", e) + except StopIteration as e: + self.log.debug("Closing connection. %s", e) + except ssl.SSLError as e: + if e.args[0] == ssl.SSL_ERROR_EOF: + self.log.debug("ssl connection closed") + client.close() + else: + self.log.debug("Error processing SSL request.") + self.handle_error(req, client, addr, e) + except OSError as e: + if e.errno not in (errno.EPIPE, errno.ECONNRESET, errno.ENOTCONN): + self.log.exception("Socket error processing request.") + else: + if e.errno == errno.ECONNRESET: + self.log.debug("Ignoring connection reset") + elif e.errno == errno.ENOTCONN: + self.log.debug("Ignoring socket not connected") + else: + self.log.debug("Ignoring EPIPE") + except BaseException as e: + self.handle_error(req, client, addr, e) + finally: + util.close(client) + + def handle_request(self, listener, req, client, addr): + environ = {} + resp = None + try: + self.cfg.pre_request(self, req) + request_start = datetime.now() + resp, environ = wsgi.create(req, client, addr, + listener.getsockname(), self.cfg) + # Force the connection closed until someone shows + # a buffering proxy that supports Keep-Alive to + # the backend. + resp.force_close() + self.nr += 1 + if self.nr >= self.max_requests: + self.log.info("Autorestarting worker after current request.") + self.alive = False + respiter = self.wsgi(environ, resp.start_response) + try: + if isinstance(respiter, environ['wsgi.file_wrapper']): + resp.write_file(respiter) + else: + for item in respiter: + resp.write(item) + resp.close() + finally: + request_time = datetime.now() - request_start + self.log.access(resp, req, environ, request_time) + if hasattr(respiter, "close"): + respiter.close() + except OSError: + # pass to next try-except level + util.reraise(*sys.exc_info()) + except Exception: + if resp and resp.headers_sent: + # If the requests have already been sent, we should close the + # connection to indicate the error. + self.log.exception("Error handling request") + try: + client.shutdown(socket.SHUT_RDWR) + client.close() + except OSError: + pass + raise StopIteration() + raise + finally: + try: + self.cfg.post_request(self, req, environ, resp) + except Exception: + self.log.exception("Exception in post_request hook") diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/workers/workertmp.py b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/workertmp.py new file mode 100644 index 0000000..8ef00a5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/workers/workertmp.py @@ -0,0 +1,53 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import os +import time +import platform +import tempfile + +from gunicorn import util + +PLATFORM = platform.system() +IS_CYGWIN = PLATFORM.startswith('CYGWIN') + + +class WorkerTmp: + + def __init__(self, cfg): + old_umask = os.umask(cfg.umask) + fdir = cfg.worker_tmp_dir + if fdir and not os.path.isdir(fdir): + raise RuntimeError("%s doesn't exist. Can't create workertmp." % fdir) + fd, name = tempfile.mkstemp(prefix="wgunicorn-", dir=fdir) + os.umask(old_umask) + + # change the owner and group of the file if the worker will run as + # a different user or group, so that the worker can modify the file + if cfg.uid != os.geteuid() or cfg.gid != os.getegid(): + util.chown(name, cfg.uid, cfg.gid) + + # unlink the file so we don't leak temporary files + try: + if not IS_CYGWIN: + util.unlink(name) + # In Python 3.8, open() emits RuntimeWarning if buffering=1 for binary mode. + # Because we never write to this file, pass 0 to switch buffering off. + self._tmp = os.fdopen(fd, 'w+b', 0) + except Exception: + os.close(fd) + raise + + def notify(self): + new_time = time.monotonic() + os.utime(self._tmp.fileno(), (new_time, new_time)) + + def last_update(self): + return os.fstat(self._tmp.fileno()).st_mtime + + def fileno(self): + return self._tmp.fileno() + + def close(self): + return self._tmp.close() diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER b/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt b/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt new file mode 100644 index 0000000..7b190ca --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2011 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/METADATA b/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/METADATA new file mode 100644 index 0000000..ddf5464 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/METADATA @@ -0,0 +1,60 @@ +Metadata-Version: 2.1 +Name: itsdangerous +Version: 2.2.0 +Summary: Safely pass data to untrusted environments and back. +Maintainer-email: Pallets +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Typing :: Typed +Project-URL: Changes, https://itsdangerous.palletsprojects.com/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://itsdangerous.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/itsdangerous/ + +# ItsDangerous + +... so better sign this + +Various helpers to pass data to untrusted environments and to get it +back safe and sound. Data is cryptographically signed to ensure that a +token has not been tampered with. + +It's possible to customize how data is serialized. Data is compressed as +needed. A timestamp can be added and verified automatically while +loading a token. + + +## A Simple Example + +Here's how you could generate a token for transmitting a user's id and +name between web requests. + +```python +from itsdangerous import URLSafeSerializer +auth_s = URLSafeSerializer("secret key", "auth") +token = auth_s.dumps({"id": 5, "name": "itsdangerous"}) + +print(token) +# eyJpZCI6NSwibmFtZSI6Iml0c2Rhbmdlcm91cyJ9.6YP6T0BaO67XP--9UzTrmurXSmg + +data = auth_s.loads(token) +print(data["name"]) +# itsdangerous +``` + + +## Donate + +The Pallets organization develops and supports ItsDangerous and other +popular packages. In order to grow the community of contributors and +users, and allow the maintainers to devote more time to the projects, +[please donate today][]. + +[please donate today]: https://palletsprojects.com/donate + diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/RECORD b/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/RECORD new file mode 100644 index 0000000..333875c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/RECORD @@ -0,0 +1,22 @@ +itsdangerous-2.2.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +itsdangerous-2.2.0.dist-info/LICENSE.txt,sha256=Y68JiRtr6K0aQlLtQ68PTvun_JSOIoNnvtfzxa4LCdc,1475 +itsdangerous-2.2.0.dist-info/METADATA,sha256=0rk0-1ZwihuU5DnwJVwPWoEI4yWOyCexih3JyZHblhE,1924 +itsdangerous-2.2.0.dist-info/RECORD,, +itsdangerous-2.2.0.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 +itsdangerous/__init__.py,sha256=4SK75sCe29xbRgQE1ZQtMHnKUuZYAf3bSpZOrff1IAY,1427 +itsdangerous/__pycache__/__init__.cpython-311.pyc,, +itsdangerous/__pycache__/_json.cpython-311.pyc,, +itsdangerous/__pycache__/encoding.cpython-311.pyc,, +itsdangerous/__pycache__/exc.cpython-311.pyc,, +itsdangerous/__pycache__/serializer.cpython-311.pyc,, +itsdangerous/__pycache__/signer.cpython-311.pyc,, +itsdangerous/__pycache__/timed.cpython-311.pyc,, +itsdangerous/__pycache__/url_safe.cpython-311.pyc,, +itsdangerous/_json.py,sha256=wPQGmge2yZ9328EHKF6gadGeyGYCJQKxtU-iLKE6UnA,473 +itsdangerous/encoding.py,sha256=wwTz5q_3zLcaAdunk6_vSoStwGqYWe307Zl_U87aRFM,1409 +itsdangerous/exc.py,sha256=Rr3exo0MRFEcPZltwecyK16VV1bE2K9_F1-d-ljcUn4,3201 +itsdangerous/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +itsdangerous/serializer.py,sha256=PmdwADLqkSyQLZ0jOKAgDsAW4k_H0TlA71Ei3z0C5aI,15601 +itsdangerous/signer.py,sha256=YO0CV7NBvHA6j549REHJFUjUojw2pHqwcUpQnU7yNYQ,9647 +itsdangerous/timed.py,sha256=6RvDMqNumGMxf0-HlpaZdN9PUQQmRvrQGplKhxuivUs,8083 +itsdangerous/url_safe.py,sha256=az4e5fXi_vs-YbWj8YZwn4wiVKfeD--GEKRT5Ueu4P4,2505 diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/WHEEL b/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/WHEEL new file mode 100644 index 0000000..3b5e64b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.9.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous/__init__.py b/netdeploy/lib/python3.11/site-packages/itsdangerous/__init__.py new file mode 100644 index 0000000..ea55256 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous/__init__.py @@ -0,0 +1,38 @@ +from __future__ import annotations + +import typing as t + +from .encoding import base64_decode as base64_decode +from .encoding import base64_encode as base64_encode +from .encoding import want_bytes as want_bytes +from .exc import BadData as BadData +from .exc import BadHeader as BadHeader +from .exc import BadPayload as BadPayload +from .exc import BadSignature as BadSignature +from .exc import BadTimeSignature as BadTimeSignature +from .exc import SignatureExpired as SignatureExpired +from .serializer import Serializer as Serializer +from .signer import HMACAlgorithm as HMACAlgorithm +from .signer import NoneAlgorithm as NoneAlgorithm +from .signer import Signer as Signer +from .timed import TimedSerializer as TimedSerializer +from .timed import TimestampSigner as TimestampSigner +from .url_safe import URLSafeSerializer as URLSafeSerializer +from .url_safe import URLSafeTimedSerializer as URLSafeTimedSerializer + + +def __getattr__(name: str) -> t.Any: + if name == "__version__": + import importlib.metadata + import warnings + + warnings.warn( + "The '__version__' attribute is deprecated and will be removed in" + " ItsDangerous 2.3. Use feature detection or" + " 'importlib.metadata.version(\"itsdangerous\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("itsdangerous") + + raise AttributeError(name) diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous/_json.py b/netdeploy/lib/python3.11/site-packages/itsdangerous/_json.py new file mode 100644 index 0000000..fc23fea --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous/_json.py @@ -0,0 +1,18 @@ +from __future__ import annotations + +import json as _json +import typing as t + + +class _CompactJSON: + """Wrapper around json module that strips whitespace.""" + + @staticmethod + def loads(payload: str | bytes) -> t.Any: + return _json.loads(payload) + + @staticmethod + def dumps(obj: t.Any, **kwargs: t.Any) -> str: + kwargs.setdefault("ensure_ascii", False) + kwargs.setdefault("separators", (",", ":")) + return _json.dumps(obj, **kwargs) diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous/encoding.py b/netdeploy/lib/python3.11/site-packages/itsdangerous/encoding.py new file mode 100644 index 0000000..f5ca80f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous/encoding.py @@ -0,0 +1,54 @@ +from __future__ import annotations + +import base64 +import string +import struct +import typing as t + +from .exc import BadData + + +def want_bytes( + s: str | bytes, encoding: str = "utf-8", errors: str = "strict" +) -> bytes: + if isinstance(s, str): + s = s.encode(encoding, errors) + + return s + + +def base64_encode(string: str | bytes) -> bytes: + """Base64 encode a string of bytes or text. The resulting bytes are + safe to use in URLs. + """ + string = want_bytes(string) + return base64.urlsafe_b64encode(string).rstrip(b"=") + + +def base64_decode(string: str | bytes) -> bytes: + """Base64 decode a URL-safe string of bytes or text. The result is + bytes. + """ + string = want_bytes(string, encoding="ascii", errors="ignore") + string += b"=" * (-len(string) % 4) + + try: + return base64.urlsafe_b64decode(string) + except (TypeError, ValueError) as e: + raise BadData("Invalid base64-encoded data") from e + + +# The alphabet used by base64.urlsafe_* +_base64_alphabet = f"{string.ascii_letters}{string.digits}-_=".encode("ascii") + +_int64_struct = struct.Struct(">Q") +_int_to_bytes = _int64_struct.pack +_bytes_to_int = t.cast("t.Callable[[bytes], tuple[int]]", _int64_struct.unpack) + + +def int_to_bytes(num: int) -> bytes: + return _int_to_bytes(num).lstrip(b"\x00") + + +def bytes_to_int(bytestr: bytes) -> int: + return _bytes_to_int(bytestr.rjust(8, b"\x00"))[0] diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous/exc.py b/netdeploy/lib/python3.11/site-packages/itsdangerous/exc.py new file mode 100644 index 0000000..a75adcd --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous/exc.py @@ -0,0 +1,106 @@ +from __future__ import annotations + +import typing as t +from datetime import datetime + + +class BadData(Exception): + """Raised if bad data of any sort was encountered. This is the base + for all exceptions that ItsDangerous defines. + + .. versionadded:: 0.15 + """ + + def __init__(self, message: str): + super().__init__(message) + self.message = message + + def __str__(self) -> str: + return self.message + + +class BadSignature(BadData): + """Raised if a signature does not match.""" + + def __init__(self, message: str, payload: t.Any | None = None): + super().__init__(message) + + #: The payload that failed the signature test. In some + #: situations you might still want to inspect this, even if + #: you know it was tampered with. + #: + #: .. versionadded:: 0.14 + self.payload: t.Any | None = payload + + +class BadTimeSignature(BadSignature): + """Raised if a time-based signature is invalid. This is a subclass + of :class:`BadSignature`. + """ + + def __init__( + self, + message: str, + payload: t.Any | None = None, + date_signed: datetime | None = None, + ): + super().__init__(message, payload) + + #: If the signature expired this exposes the date of when the + #: signature was created. This can be helpful in order to + #: tell the user how long a link has been gone stale. + #: + #: .. versionchanged:: 2.0 + #: The datetime value is timezone-aware rather than naive. + #: + #: .. versionadded:: 0.14 + self.date_signed = date_signed + + +class SignatureExpired(BadTimeSignature): + """Raised if a signature timestamp is older than ``max_age``. This + is a subclass of :exc:`BadTimeSignature`. + """ + + +class BadHeader(BadSignature): + """Raised if a signed header is invalid in some form. This only + happens for serializers that have a header that goes with the + signature. + + .. versionadded:: 0.24 + """ + + def __init__( + self, + message: str, + payload: t.Any | None = None, + header: t.Any | None = None, + original_error: Exception | None = None, + ): + super().__init__(message, payload) + + #: If the header is actually available but just malformed it + #: might be stored here. + self.header: t.Any | None = header + + #: If available, the error that indicates why the payload was + #: not valid. This might be ``None``. + self.original_error: Exception | None = original_error + + +class BadPayload(BadData): + """Raised if a payload is invalid. This could happen if the payload + is loaded despite an invalid signature, or if there is a mismatch + between the serializer and deserializer. The original exception + that occurred during loading is stored on as :attr:`original_error`. + + .. versionadded:: 0.15 + """ + + def __init__(self, message: str, original_error: Exception | None = None): + super().__init__(message) + + #: If available, the error that indicates why the payload was + #: not valid. This might be ``None``. + self.original_error: Exception | None = original_error diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous/py.typed b/netdeploy/lib/python3.11/site-packages/itsdangerous/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous/serializer.py b/netdeploy/lib/python3.11/site-packages/itsdangerous/serializer.py new file mode 100644 index 0000000..5ddf387 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous/serializer.py @@ -0,0 +1,406 @@ +from __future__ import annotations + +import collections.abc as cabc +import json +import typing as t + +from .encoding import want_bytes +from .exc import BadPayload +from .exc import BadSignature +from .signer import _make_keys_list +from .signer import Signer + +if t.TYPE_CHECKING: + import typing_extensions as te + + # This should be either be str or bytes. To avoid having to specify the + # bound type, it falls back to a union if structural matching fails. + _TSerialized = te.TypeVar( + "_TSerialized", bound=t.Union[str, bytes], default=t.Union[str, bytes] + ) +else: + # Still available at runtime on Python < 3.13, but without the default. + _TSerialized = t.TypeVar("_TSerialized", bound=t.Union[str, bytes]) + + +class _PDataSerializer(t.Protocol[_TSerialized]): + def loads(self, payload: _TSerialized, /) -> t.Any: ... + # A signature with additional arguments is not handled correctly by type + # checkers right now, so an overload is used below for serializers that + # don't match this strict protocol. + def dumps(self, obj: t.Any, /) -> _TSerialized: ... + + +# Use TypeIs once it's available in typing_extensions or 3.13. +def is_text_serializer( + serializer: _PDataSerializer[t.Any], +) -> te.TypeGuard[_PDataSerializer[str]]: + """Checks whether a serializer generates text or binary.""" + return isinstance(serializer.dumps({}), str) + + +class Serializer(t.Generic[_TSerialized]): + """A serializer wraps a :class:`~itsdangerous.signer.Signer` to + enable serializing and securely signing data other than bytes. It + can unsign to verify that the data hasn't been changed. + + The serializer provides :meth:`dumps` and :meth:`loads`, similar to + :mod:`json`, and by default uses :mod:`json` internally to serialize + the data to bytes. + + The secret key should be a random string of ``bytes`` and should not + be saved to code or version control. Different salts should be used + to distinguish signing in different contexts. See :doc:`/concepts` + for information about the security of the secret key and salt. + + :param secret_key: The secret key to sign and verify with. Can be a + list of keys, oldest to newest, to support key rotation. + :param salt: Extra key to combine with ``secret_key`` to distinguish + signatures in different contexts. + :param serializer: An object that provides ``dumps`` and ``loads`` + methods for serializing data to a string. Defaults to + :attr:`default_serializer`, which defaults to :mod:`json`. + :param serializer_kwargs: Keyword arguments to pass when calling + ``serializer.dumps``. + :param signer: A ``Signer`` class to instantiate when signing data. + Defaults to :attr:`default_signer`, which defaults to + :class:`~itsdangerous.signer.Signer`. + :param signer_kwargs: Keyword arguments to pass when instantiating + the ``Signer`` class. + :param fallback_signers: List of signer parameters to try when + unsigning with the default signer fails. Each item can be a dict + of ``signer_kwargs``, a ``Signer`` class, or a tuple of + ``(signer, signer_kwargs)``. Defaults to + :attr:`default_fallback_signers`. + + .. versionchanged:: 2.0 + Added support for key rotation by passing a list to + ``secret_key``. + + .. versionchanged:: 2.0 + Removed the default SHA-512 fallback signer from + ``default_fallback_signers``. + + .. versionchanged:: 1.1 + Added support for ``fallback_signers`` and configured a default + SHA-512 fallback. This fallback is for users who used the yanked + 1.0.0 release which defaulted to SHA-512. + + .. versionchanged:: 0.14 + The ``signer`` and ``signer_kwargs`` parameters were added to + the constructor. + """ + + #: The default serialization module to use to serialize data to a + #: string internally. The default is :mod:`json`, but can be changed + #: to any object that provides ``dumps`` and ``loads`` methods. + default_serializer: _PDataSerializer[t.Any] = json + + #: The default ``Signer`` class to instantiate when signing data. + #: The default is :class:`itsdangerous.signer.Signer`. + default_signer: type[Signer] = Signer + + #: The default fallback signers to try when unsigning fails. + default_fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] = [] + + # Serializer[str] if no data serializer is provided, or if it returns str. + @t.overload + def __init__( + self: Serializer[str], + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + serializer: None | _PDataSerializer[str] = None, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Serializer[bytes] with a bytes data serializer positional argument. + @t.overload + def __init__( + self: Serializer[bytes], + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None, + serializer: _PDataSerializer[bytes], + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Serializer[bytes] with a bytes data serializer keyword argument. + @t.overload + def __init__( + self: Serializer[bytes], + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + *, + serializer: _PDataSerializer[bytes], + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Fall back with a positional argument. If the strict signature of + # _PDataSerializer doesn't match, fall back to a union, requiring the user + # to specify the type. + @t.overload + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None, + serializer: t.Any, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + # Fall back with a keyword argument. + @t.overload + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + *, + serializer: t.Any, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): ... + + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous", + serializer: t.Any | None = None, + serializer_kwargs: dict[str, t.Any] | None = None, + signer: type[Signer] | None = None, + signer_kwargs: dict[str, t.Any] | None = None, + fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] + | None = None, + ): + #: The list of secret keys to try for verifying signatures, from + #: oldest to newest. The newest (last) key is used for signing. + #: + #: This allows a key rotation system to keep a list of allowed + #: keys and remove expired ones. + self.secret_keys: list[bytes] = _make_keys_list(secret_key) + + if salt is not None: + salt = want_bytes(salt) + # if salt is None then the signer's default is used + + self.salt = salt + + if serializer is None: + serializer = self.default_serializer + + self.serializer: _PDataSerializer[_TSerialized] = serializer + self.is_text_serializer: bool = is_text_serializer(serializer) + + if signer is None: + signer = self.default_signer + + self.signer: type[Signer] = signer + self.signer_kwargs: dict[str, t.Any] = signer_kwargs or {} + + if fallback_signers is None: + fallback_signers = list(self.default_fallback_signers) + + self.fallback_signers: list[ + dict[str, t.Any] | tuple[type[Signer], dict[str, t.Any]] | type[Signer] + ] = fallback_signers + self.serializer_kwargs: dict[str, t.Any] = serializer_kwargs or {} + + @property + def secret_key(self) -> bytes: + """The newest (last) entry in the :attr:`secret_keys` list. This + is for compatibility from before key rotation support was added. + """ + return self.secret_keys[-1] + + def load_payload( + self, payload: bytes, serializer: _PDataSerializer[t.Any] | None = None + ) -> t.Any: + """Loads the encoded object. This function raises + :class:`.BadPayload` if the payload is not valid. The + ``serializer`` parameter can be used to override the serializer + stored on the class. The encoded ``payload`` should always be + bytes. + """ + if serializer is None: + use_serializer = self.serializer + is_text = self.is_text_serializer + else: + use_serializer = serializer + is_text = is_text_serializer(serializer) + + try: + if is_text: + return use_serializer.loads(payload.decode("utf-8")) # type: ignore[arg-type] + + return use_serializer.loads(payload) # type: ignore[arg-type] + except Exception as e: + raise BadPayload( + "Could not load the payload because an exception" + " occurred on unserializing the data.", + original_error=e, + ) from e + + def dump_payload(self, obj: t.Any) -> bytes: + """Dumps the encoded object. The return value is always bytes. + If the internal serializer returns text, the value will be + encoded as UTF-8. + """ + return want_bytes(self.serializer.dumps(obj, **self.serializer_kwargs)) + + def make_signer(self, salt: str | bytes | None = None) -> Signer: + """Creates a new instance of the signer to be used. The default + implementation uses the :class:`.Signer` base class. + """ + if salt is None: + salt = self.salt + + return self.signer(self.secret_keys, salt=salt, **self.signer_kwargs) + + def iter_unsigners(self, salt: str | bytes | None = None) -> cabc.Iterator[Signer]: + """Iterates over all signers to be tried for unsigning. Starts + with the configured signer, then constructs each signer + specified in ``fallback_signers``. + """ + if salt is None: + salt = self.salt + + yield self.make_signer(salt) + + for fallback in self.fallback_signers: + if isinstance(fallback, dict): + kwargs = fallback + fallback = self.signer + elif isinstance(fallback, tuple): + fallback, kwargs = fallback + else: + kwargs = self.signer_kwargs + + for secret_key in self.secret_keys: + yield fallback(secret_key, salt=salt, **kwargs) + + def dumps(self, obj: t.Any, salt: str | bytes | None = None) -> _TSerialized: + """Returns a signed string serialized with the internal + serializer. The return value can be either a byte or unicode + string depending on the format of the internal serializer. + """ + payload = want_bytes(self.dump_payload(obj)) + rv = self.make_signer(salt).sign(payload) + + if self.is_text_serializer: + return rv.decode("utf-8") # type: ignore[return-value] + + return rv # type: ignore[return-value] + + def dump(self, obj: t.Any, f: t.IO[t.Any], salt: str | bytes | None = None) -> None: + """Like :meth:`dumps` but dumps into a file. The file handle has + to be compatible with what the internal serializer expects. + """ + f.write(self.dumps(obj, salt)) + + def loads( + self, s: str | bytes, salt: str | bytes | None = None, **kwargs: t.Any + ) -> t.Any: + """Reverse of :meth:`dumps`. Raises :exc:`.BadSignature` if the + signature validation fails. + """ + s = want_bytes(s) + last_exception = None + + for signer in self.iter_unsigners(salt): + try: + return self.load_payload(signer.unsign(s)) + except BadSignature as err: + last_exception = err + + raise t.cast(BadSignature, last_exception) + + def load(self, f: t.IO[t.Any], salt: str | bytes | None = None) -> t.Any: + """Like :meth:`loads` but loads from a file.""" + return self.loads(f.read(), salt) + + def loads_unsafe( + self, s: str | bytes, salt: str | bytes | None = None + ) -> tuple[bool, t.Any]: + """Like :meth:`loads` but without verifying the signature. This + is potentially very dangerous to use depending on how your + serializer works. The return value is ``(signature_valid, + payload)`` instead of just the payload. The first item will be a + boolean that indicates if the signature is valid. This function + never fails. + + Use it for debugging only and if you know that your serializer + module is not exploitable (for example, do not use it with a + pickle serializer). + + .. versionadded:: 0.15 + """ + return self._loads_unsafe_impl(s, salt) + + def _loads_unsafe_impl( + self, + s: str | bytes, + salt: str | bytes | None, + load_kwargs: dict[str, t.Any] | None = None, + load_payload_kwargs: dict[str, t.Any] | None = None, + ) -> tuple[bool, t.Any]: + """Low level helper function to implement :meth:`loads_unsafe` + in serializer subclasses. + """ + if load_kwargs is None: + load_kwargs = {} + + try: + return True, self.loads(s, salt=salt, **load_kwargs) + except BadSignature as e: + if e.payload is None: + return False, None + + if load_payload_kwargs is None: + load_payload_kwargs = {} + + try: + return ( + False, + self.load_payload(e.payload, **load_payload_kwargs), + ) + except BadPayload: + return False, None + + def load_unsafe( + self, f: t.IO[t.Any], salt: str | bytes | None = None + ) -> tuple[bool, t.Any]: + """Like :meth:`loads_unsafe` but loads from a file. + + .. versionadded:: 0.15 + """ + return self.loads_unsafe(f.read(), salt=salt) diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous/signer.py b/netdeploy/lib/python3.11/site-packages/itsdangerous/signer.py new file mode 100644 index 0000000..e324dc0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous/signer.py @@ -0,0 +1,266 @@ +from __future__ import annotations + +import collections.abc as cabc +import hashlib +import hmac +import typing as t + +from .encoding import _base64_alphabet +from .encoding import base64_decode +from .encoding import base64_encode +from .encoding import want_bytes +from .exc import BadSignature + + +class SigningAlgorithm: + """Subclasses must implement :meth:`get_signature` to provide + signature generation functionality. + """ + + def get_signature(self, key: bytes, value: bytes) -> bytes: + """Returns the signature for the given key and value.""" + raise NotImplementedError() + + def verify_signature(self, key: bytes, value: bytes, sig: bytes) -> bool: + """Verifies the given signature matches the expected + signature. + """ + return hmac.compare_digest(sig, self.get_signature(key, value)) + + +class NoneAlgorithm(SigningAlgorithm): + """Provides an algorithm that does not perform any signing and + returns an empty signature. + """ + + def get_signature(self, key: bytes, value: bytes) -> bytes: + return b"" + + +def _lazy_sha1(string: bytes = b"") -> t.Any: + """Don't access ``hashlib.sha1`` until runtime. FIPS builds may not include + SHA-1, in which case the import and use as a default would fail before the + developer can configure something else. + """ + return hashlib.sha1(string) + + +class HMACAlgorithm(SigningAlgorithm): + """Provides signature generation using HMACs.""" + + #: The digest method to use with the MAC algorithm. This defaults to + #: SHA1, but can be changed to any other function in the hashlib + #: module. + default_digest_method: t.Any = staticmethod(_lazy_sha1) + + def __init__(self, digest_method: t.Any = None): + if digest_method is None: + digest_method = self.default_digest_method + + self.digest_method: t.Any = digest_method + + def get_signature(self, key: bytes, value: bytes) -> bytes: + mac = hmac.new(key, msg=value, digestmod=self.digest_method) + return mac.digest() + + +def _make_keys_list( + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], +) -> list[bytes]: + if isinstance(secret_key, (str, bytes)): + return [want_bytes(secret_key)] + + return [want_bytes(s) for s in secret_key] # pyright: ignore + + +class Signer: + """A signer securely signs bytes, then unsigns them to verify that + the value hasn't been changed. + + The secret key should be a random string of ``bytes`` and should not + be saved to code or version control. Different salts should be used + to distinguish signing in different contexts. See :doc:`/concepts` + for information about the security of the secret key and salt. + + :param secret_key: The secret key to sign and verify with. Can be a + list of keys, oldest to newest, to support key rotation. + :param salt: Extra key to combine with ``secret_key`` to distinguish + signatures in different contexts. + :param sep: Separator between the signature and value. + :param key_derivation: How to derive the signing key from the secret + key and salt. Possible values are ``concat``, ``django-concat``, + or ``hmac``. Defaults to :attr:`default_key_derivation`, which + defaults to ``django-concat``. + :param digest_method: Hash function to use when generating the HMAC + signature. Defaults to :attr:`default_digest_method`, which + defaults to :func:`hashlib.sha1`. Note that the security of the + hash alone doesn't apply when used intermediately in HMAC. + :param algorithm: A :class:`SigningAlgorithm` instance to use + instead of building a default :class:`HMACAlgorithm` with the + ``digest_method``. + + .. versionchanged:: 2.0 + Added support for key rotation by passing a list to + ``secret_key``. + + .. versionchanged:: 0.18 + ``algorithm`` was added as an argument to the class constructor. + + .. versionchanged:: 0.14 + ``key_derivation`` and ``digest_method`` were added as arguments + to the class constructor. + """ + + #: The default digest method to use for the signer. The default is + #: :func:`hashlib.sha1`, but can be changed to any :mod:`hashlib` or + #: compatible object. Note that the security of the hash alone + #: doesn't apply when used intermediately in HMAC. + #: + #: .. versionadded:: 0.14 + default_digest_method: t.Any = staticmethod(_lazy_sha1) + + #: The default scheme to use to derive the signing key from the + #: secret key and salt. The default is ``django-concat``. Possible + #: values are ``concat``, ``django-concat``, and ``hmac``. + #: + #: .. versionadded:: 0.14 + default_key_derivation: str = "django-concat" + + def __init__( + self, + secret_key: str | bytes | cabc.Iterable[str] | cabc.Iterable[bytes], + salt: str | bytes | None = b"itsdangerous.Signer", + sep: str | bytes = b".", + key_derivation: str | None = None, + digest_method: t.Any | None = None, + algorithm: SigningAlgorithm | None = None, + ): + #: The list of secret keys to try for verifying signatures, from + #: oldest to newest. The newest (last) key is used for signing. + #: + #: This allows a key rotation system to keep a list of allowed + #: keys and remove expired ones. + self.secret_keys: list[bytes] = _make_keys_list(secret_key) + self.sep: bytes = want_bytes(sep) + + if self.sep in _base64_alphabet: + raise ValueError( + "The given separator cannot be used because it may be" + " contained in the signature itself. ASCII letters," + " digits, and '-_=' must not be used." + ) + + if salt is not None: + salt = want_bytes(salt) + else: + salt = b"itsdangerous.Signer" + + self.salt = salt + + if key_derivation is None: + key_derivation = self.default_key_derivation + + self.key_derivation: str = key_derivation + + if digest_method is None: + digest_method = self.default_digest_method + + self.digest_method: t.Any = digest_method + + if algorithm is None: + algorithm = HMACAlgorithm(self.digest_method) + + self.algorithm: SigningAlgorithm = algorithm + + @property + def secret_key(self) -> bytes: + """The newest (last) entry in the :attr:`secret_keys` list. This + is for compatibility from before key rotation support was added. + """ + return self.secret_keys[-1] + + def derive_key(self, secret_key: str | bytes | None = None) -> bytes: + """This method is called to derive the key. The default key + derivation choices can be overridden here. Key derivation is not + intended to be used as a security method to make a complex key + out of a short password. Instead you should use large random + secret keys. + + :param secret_key: A specific secret key to derive from. + Defaults to the last item in :attr:`secret_keys`. + + .. versionchanged:: 2.0 + Added the ``secret_key`` parameter. + """ + if secret_key is None: + secret_key = self.secret_keys[-1] + else: + secret_key = want_bytes(secret_key) + + if self.key_derivation == "concat": + return t.cast(bytes, self.digest_method(self.salt + secret_key).digest()) + elif self.key_derivation == "django-concat": + return t.cast( + bytes, self.digest_method(self.salt + b"signer" + secret_key).digest() + ) + elif self.key_derivation == "hmac": + mac = hmac.new(secret_key, digestmod=self.digest_method) + mac.update(self.salt) + return mac.digest() + elif self.key_derivation == "none": + return secret_key + else: + raise TypeError("Unknown key derivation method") + + def get_signature(self, value: str | bytes) -> bytes: + """Returns the signature for the given value.""" + value = want_bytes(value) + key = self.derive_key() + sig = self.algorithm.get_signature(key, value) + return base64_encode(sig) + + def sign(self, value: str | bytes) -> bytes: + """Signs the given string.""" + value = want_bytes(value) + return value + self.sep + self.get_signature(value) + + def verify_signature(self, value: str | bytes, sig: str | bytes) -> bool: + """Verifies the signature for the given value.""" + try: + sig = base64_decode(sig) + except Exception: + return False + + value = want_bytes(value) + + for secret_key in reversed(self.secret_keys): + key = self.derive_key(secret_key) + + if self.algorithm.verify_signature(key, value, sig): + return True + + return False + + def unsign(self, signed_value: str | bytes) -> bytes: + """Unsigns the given string.""" + signed_value = want_bytes(signed_value) + + if self.sep not in signed_value: + raise BadSignature(f"No {self.sep!r} found in value") + + value, sig = signed_value.rsplit(self.sep, 1) + + if self.verify_signature(value, sig): + return value + + raise BadSignature(f"Signature {sig!r} does not match", payload=value) + + def validate(self, signed_value: str | bytes) -> bool: + """Only validates the given signed value. Returns ``True`` if + the signature exists and is valid. + """ + try: + self.unsign(signed_value) + return True + except BadSignature: + return False diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous/timed.py b/netdeploy/lib/python3.11/site-packages/itsdangerous/timed.py new file mode 100644 index 0000000..7384375 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous/timed.py @@ -0,0 +1,228 @@ +from __future__ import annotations + +import collections.abc as cabc +import time +import typing as t +from datetime import datetime +from datetime import timezone + +from .encoding import base64_decode +from .encoding import base64_encode +from .encoding import bytes_to_int +from .encoding import int_to_bytes +from .encoding import want_bytes +from .exc import BadSignature +from .exc import BadTimeSignature +from .exc import SignatureExpired +from .serializer import _TSerialized +from .serializer import Serializer +from .signer import Signer + + +class TimestampSigner(Signer): + """Works like the regular :class:`.Signer` but also records the time + of the signing and can be used to expire signatures. The + :meth:`unsign` method can raise :exc:`.SignatureExpired` if the + unsigning failed because the signature is expired. + """ + + def get_timestamp(self) -> int: + """Returns the current timestamp. The function must return an + integer. + """ + return int(time.time()) + + def timestamp_to_datetime(self, ts: int) -> datetime: + """Convert the timestamp from :meth:`get_timestamp` into an + aware :class`datetime.datetime` in UTC. + + .. versionchanged:: 2.0 + The timestamp is returned as a timezone-aware ``datetime`` + in UTC rather than a naive ``datetime`` assumed to be UTC. + """ + return datetime.fromtimestamp(ts, tz=timezone.utc) + + def sign(self, value: str | bytes) -> bytes: + """Signs the given string and also attaches time information.""" + value = want_bytes(value) + timestamp = base64_encode(int_to_bytes(self.get_timestamp())) + sep = want_bytes(self.sep) + value = value + sep + timestamp + return value + sep + self.get_signature(value) + + # Ignore overlapping signatures check, return_timestamp is the only + # parameter that affects the return type. + + @t.overload + def unsign( # type: ignore[overload-overlap] + self, + signed_value: str | bytes, + max_age: int | None = None, + return_timestamp: t.Literal[False] = False, + ) -> bytes: ... + + @t.overload + def unsign( + self, + signed_value: str | bytes, + max_age: int | None = None, + return_timestamp: t.Literal[True] = True, + ) -> tuple[bytes, datetime]: ... + + def unsign( + self, + signed_value: str | bytes, + max_age: int | None = None, + return_timestamp: bool = False, + ) -> tuple[bytes, datetime] | bytes: + """Works like the regular :meth:`.Signer.unsign` but can also + validate the time. See the base docstring of the class for + the general behavior. If ``return_timestamp`` is ``True`` the + timestamp of the signature will be returned as an aware + :class:`datetime.datetime` object in UTC. + + .. versionchanged:: 2.0 + The timestamp is returned as a timezone-aware ``datetime`` + in UTC rather than a naive ``datetime`` assumed to be UTC. + """ + try: + result = super().unsign(signed_value) + sig_error = None + except BadSignature as e: + sig_error = e + result = e.payload or b"" + + sep = want_bytes(self.sep) + + # If there is no timestamp in the result there is something + # seriously wrong. In case there was a signature error, we raise + # that one directly, otherwise we have a weird situation in + # which we shouldn't have come except someone uses a time-based + # serializer on non-timestamp data, so catch that. + if sep not in result: + if sig_error: + raise sig_error + + raise BadTimeSignature("timestamp missing", payload=result) + + value, ts_bytes = result.rsplit(sep, 1) + ts_int: int | None = None + ts_dt: datetime | None = None + + try: + ts_int = bytes_to_int(base64_decode(ts_bytes)) + except Exception: + pass + + # Signature is *not* okay. Raise a proper error now that we have + # split the value and the timestamp. + if sig_error is not None: + if ts_int is not None: + try: + ts_dt = self.timestamp_to_datetime(ts_int) + except (ValueError, OSError, OverflowError) as exc: + # Windows raises OSError + # 32-bit raises OverflowError + raise BadTimeSignature( + "Malformed timestamp", payload=value + ) from exc + + raise BadTimeSignature(str(sig_error), payload=value, date_signed=ts_dt) + + # Signature was okay but the timestamp is actually not there or + # malformed. Should not happen, but we handle it anyway. + if ts_int is None: + raise BadTimeSignature("Malformed timestamp", payload=value) + + # Check timestamp is not older than max_age + if max_age is not None: + age = self.get_timestamp() - ts_int + + if age > max_age: + raise SignatureExpired( + f"Signature age {age} > {max_age} seconds", + payload=value, + date_signed=self.timestamp_to_datetime(ts_int), + ) + + if age < 0: + raise SignatureExpired( + f"Signature age {age} < 0 seconds", + payload=value, + date_signed=self.timestamp_to_datetime(ts_int), + ) + + if return_timestamp: + return value, self.timestamp_to_datetime(ts_int) + + return value + + def validate(self, signed_value: str | bytes, max_age: int | None = None) -> bool: + """Only validates the given signed value. Returns ``True`` if + the signature exists and is valid.""" + try: + self.unsign(signed_value, max_age=max_age) + return True + except BadSignature: + return False + + +class TimedSerializer(Serializer[_TSerialized]): + """Uses :class:`TimestampSigner` instead of the default + :class:`.Signer`. + """ + + default_signer: type[TimestampSigner] = TimestampSigner + + def iter_unsigners( + self, salt: str | bytes | None = None + ) -> cabc.Iterator[TimestampSigner]: + return t.cast("cabc.Iterator[TimestampSigner]", super().iter_unsigners(salt)) + + # TODO: Signature is incompatible because parameters were added + # before salt. + + def loads( # type: ignore[override] + self, + s: str | bytes, + max_age: int | None = None, + return_timestamp: bool = False, + salt: str | bytes | None = None, + ) -> t.Any: + """Reverse of :meth:`dumps`, raises :exc:`.BadSignature` if the + signature validation fails. If a ``max_age`` is provided it will + ensure the signature is not older than that time in seconds. In + case the signature is outdated, :exc:`.SignatureExpired` is + raised. All arguments are forwarded to the signer's + :meth:`~TimestampSigner.unsign` method. + """ + s = want_bytes(s) + last_exception = None + + for signer in self.iter_unsigners(salt): + try: + base64d, timestamp = signer.unsign( + s, max_age=max_age, return_timestamp=True + ) + payload = self.load_payload(base64d) + + if return_timestamp: + return payload, timestamp + + return payload + except SignatureExpired: + # The signature was unsigned successfully but was + # expired. Do not try the next signer. + raise + except BadSignature as err: + last_exception = err + + raise t.cast(BadSignature, last_exception) + + def loads_unsafe( # type: ignore[override] + self, + s: str | bytes, + max_age: int | None = None, + salt: str | bytes | None = None, + ) -> tuple[bool, t.Any]: + return self._loads_unsafe_impl(s, salt, load_kwargs={"max_age": max_age}) diff --git a/netdeploy/lib/python3.11/site-packages/itsdangerous/url_safe.py b/netdeploy/lib/python3.11/site-packages/itsdangerous/url_safe.py new file mode 100644 index 0000000..56a0793 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/itsdangerous/url_safe.py @@ -0,0 +1,83 @@ +from __future__ import annotations + +import typing as t +import zlib + +from ._json import _CompactJSON +from .encoding import base64_decode +from .encoding import base64_encode +from .exc import BadPayload +from .serializer import _PDataSerializer +from .serializer import Serializer +from .timed import TimedSerializer + + +class URLSafeSerializerMixin(Serializer[str]): + """Mixed in with a regular serializer it will attempt to zlib + compress the string to make it shorter if necessary. It will also + base64 encode the string so that it can safely be placed in a URL. + """ + + default_serializer: _PDataSerializer[str] = _CompactJSON + + def load_payload( + self, + payload: bytes, + *args: t.Any, + serializer: t.Any | None = None, + **kwargs: t.Any, + ) -> t.Any: + decompress = False + + if payload.startswith(b"."): + payload = payload[1:] + decompress = True + + try: + json = base64_decode(payload) + except Exception as e: + raise BadPayload( + "Could not base64 decode the payload because of an exception", + original_error=e, + ) from e + + if decompress: + try: + json = zlib.decompress(json) + except Exception as e: + raise BadPayload( + "Could not zlib decompress the payload before decoding the payload", + original_error=e, + ) from e + + return super().load_payload(json, *args, **kwargs) + + def dump_payload(self, obj: t.Any) -> bytes: + json = super().dump_payload(obj) + is_compressed = False + compressed = zlib.compress(json) + + if len(compressed) < (len(json) - 1): + json = compressed + is_compressed = True + + base64d = base64_encode(json) + + if is_compressed: + base64d = b"." + base64d + + return base64d + + +class URLSafeSerializer(URLSafeSerializerMixin, Serializer[str]): + """Works like :class:`.Serializer` but dumps and loads into a URL + safe string consisting of the upper and lowercase character of the + alphabet as well as ``'_'``, ``'-'`` and ``'.'``. + """ + + +class URLSafeTimedSerializer(URLSafeSerializerMixin, TimedSerializer[str]): + """Works like :class:`.TimedSerializer` but dumps and loads into a + URL safe string consisting of the upper and lowercase character of + the alphabet as well as ``'_'``, ``'-'`` and ``'.'``. + """ diff --git a/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/INSTALLER b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/METADATA b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/METADATA new file mode 100644 index 0000000..ffef2ff --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/METADATA @@ -0,0 +1,84 @@ +Metadata-Version: 2.4 +Name: Jinja2 +Version: 3.1.6 +Summary: A very fast and expressive template engine. +Maintainer-email: Pallets +Requires-Python: >=3.7 +Description-Content-Type: text/markdown +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: BSD License +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Classifier: Topic :: Text Processing :: Markup :: HTML +Classifier: Typing :: Typed +License-File: LICENSE.txt +Requires-Dist: MarkupSafe>=2.0 +Requires-Dist: Babel>=2.7 ; extra == "i18n" +Project-URL: Changes, https://jinja.palletsprojects.com/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://jinja.palletsprojects.com/ +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Source, https://github.com/pallets/jinja/ +Provides-Extra: i18n + +# Jinja + +Jinja is a fast, expressive, extensible templating engine. Special +placeholders in the template allow writing code similar to Python +syntax. Then the template is passed data to render the final document. + +It includes: + +- Template inheritance and inclusion. +- Define and import macros within templates. +- HTML templates can use autoescaping to prevent XSS from untrusted + user input. +- A sandboxed environment can safely render untrusted templates. +- AsyncIO support for generating templates and calling async + functions. +- I18N support with Babel. +- Templates are compiled to optimized Python code just-in-time and + cached, or can be compiled ahead-of-time. +- Exceptions point to the correct line in templates to make debugging + easier. +- Extensible filters, tests, functions, and even syntax. + +Jinja's philosophy is that while application logic belongs in Python if +possible, it shouldn't make the template designer's job difficult by +restricting functionality too much. + + +## In A Nutshell + +```jinja +{% extends "base.html" %} +{% block title %}Members{% endblock %} +{% block content %} + +{% endblock %} +``` + +## Donate + +The Pallets organization develops and supports Jinja and other popular +packages. In order to grow the community of contributors and users, and +allow the maintainers to devote more time to the projects, [please +donate today][]. + +[please donate today]: https://palletsprojects.com/donate + +## Contributing + +See our [detailed contributing documentation][contrib] for many ways to +contribute, including reporting issues, requesting features, asking or answering +questions, and making PRs. + +[contrib]: https://palletsprojects.com/contributing/ + diff --git a/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/RECORD b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/RECORD new file mode 100644 index 0000000..4b48b91 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/RECORD @@ -0,0 +1,57 @@ +jinja2-3.1.6.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +jinja2-3.1.6.dist-info/METADATA,sha256=aMVUj7Z8QTKhOJjZsx7FDGvqKr3ZFdkh8hQ1XDpkmcg,2871 +jinja2-3.1.6.dist-info/RECORD,, +jinja2-3.1.6.dist-info/WHEEL,sha256=_2ozNFCLWc93bK4WKHCO-eDUENDlo-dgc9cU3qokYO4,82 +jinja2-3.1.6.dist-info/entry_points.txt,sha256=OL85gYU1eD8cuPlikifFngXpeBjaxl6rIJ8KkC_3r-I,58 +jinja2-3.1.6.dist-info/licenses/LICENSE.txt,sha256=O0nc7kEF6ze6wQ-vG-JgQI_oXSUrjp3y4JefweCUQ3s,1475 +jinja2/__init__.py,sha256=xxepO9i7DHsqkQrgBEduLtfoz2QCuT6_gbL4XSN1hbU,1928 +jinja2/__pycache__/__init__.cpython-311.pyc,, +jinja2/__pycache__/_identifier.cpython-311.pyc,, +jinja2/__pycache__/async_utils.cpython-311.pyc,, +jinja2/__pycache__/bccache.cpython-311.pyc,, +jinja2/__pycache__/compiler.cpython-311.pyc,, +jinja2/__pycache__/constants.cpython-311.pyc,, +jinja2/__pycache__/debug.cpython-311.pyc,, +jinja2/__pycache__/defaults.cpython-311.pyc,, +jinja2/__pycache__/environment.cpython-311.pyc,, +jinja2/__pycache__/exceptions.cpython-311.pyc,, +jinja2/__pycache__/ext.cpython-311.pyc,, +jinja2/__pycache__/filters.cpython-311.pyc,, +jinja2/__pycache__/idtracking.cpython-311.pyc,, +jinja2/__pycache__/lexer.cpython-311.pyc,, +jinja2/__pycache__/loaders.cpython-311.pyc,, +jinja2/__pycache__/meta.cpython-311.pyc,, +jinja2/__pycache__/nativetypes.cpython-311.pyc,, +jinja2/__pycache__/nodes.cpython-311.pyc,, +jinja2/__pycache__/optimizer.cpython-311.pyc,, +jinja2/__pycache__/parser.cpython-311.pyc,, +jinja2/__pycache__/runtime.cpython-311.pyc,, +jinja2/__pycache__/sandbox.cpython-311.pyc,, +jinja2/__pycache__/tests.cpython-311.pyc,, +jinja2/__pycache__/utils.cpython-311.pyc,, +jinja2/__pycache__/visitor.cpython-311.pyc,, +jinja2/_identifier.py,sha256=_zYctNKzRqlk_murTNlzrju1FFJL7Va_Ijqqd7ii2lU,1958 +jinja2/async_utils.py,sha256=vK-PdsuorOMnWSnEkT3iUJRIkTnYgO2T6MnGxDgHI5o,2834 +jinja2/bccache.py,sha256=gh0qs9rulnXo0PhX5jTJy2UHzI8wFnQ63o_vw7nhzRg,14061 +jinja2/compiler.py,sha256=9RpCQl5X88BHllJiPsHPh295Hh0uApvwFJNQuutULeM,74131 +jinja2/constants.py,sha256=GMoFydBF_kdpaRKPoM5cl5MviquVRLVyZtfp5-16jg0,1433 +jinja2/debug.py,sha256=CnHqCDHd-BVGvti_8ZsTolnXNhA3ECsY-6n_2pwU8Hw,6297 +jinja2/defaults.py,sha256=boBcSw78h-lp20YbaXSJsqkAI2uN_mD_TtCydpeq5wU,1267 +jinja2/environment.py,sha256=9nhrP7Ch-NbGX00wvyr4yy-uhNHq2OCc60ggGrni_fk,61513 +jinja2/exceptions.py,sha256=ioHeHrWwCWNaXX1inHmHVblvc4haO7AXsjCp3GfWvx0,5071 +jinja2/ext.py,sha256=5PF5eHfh8mXAIxXHHRB2xXbXohi8pE3nHSOxa66uS7E,31875 +jinja2/filters.py,sha256=PQ_Egd9n9jSgtnGQYyF4K5j2nYwhUIulhPnyimkdr-k,55212 +jinja2/idtracking.py,sha256=-ll5lIp73pML3ErUYiIJj7tdmWxcH_IlDv3yA_hiZYo,10555 +jinja2/lexer.py,sha256=LYiYio6br-Tep9nPcupWXsPEtjluw3p1mU-lNBVRUfk,29786 +jinja2/loaders.py,sha256=wIrnxjvcbqh5VwW28NSkfotiDq8qNCxIOSFbGUiSLB4,24055 +jinja2/meta.py,sha256=OTDPkaFvU2Hgvx-6akz7154F8BIWaRmvJcBFvwopHww,4397 +jinja2/nativetypes.py,sha256=7GIGALVJgdyL80oZJdQUaUfwSt5q2lSSZbXt0dNf_M4,4210 +jinja2/nodes.py,sha256=m1Duzcr6qhZI8JQ6VyJgUNinjAf5bQzijSmDnMsvUx8,34579 +jinja2/optimizer.py,sha256=rJnCRlQ7pZsEEmMhsQDgC_pKyDHxP5TPS6zVPGsgcu8,1651 +jinja2/parser.py,sha256=lLOFy3sEmHc5IaEHRiH1sQVnId2moUQzhyeJZTtdY30,40383 +jinja2/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +jinja2/runtime.py,sha256=gDk-GvdriJXqgsGbHgrcKTP0Yp6zPXzhzrIpCFH3jAU,34249 +jinja2/sandbox.py,sha256=Mw2aitlY2I8la7FYhcX2YG9BtUYcLnD0Gh3d29cDWrY,15009 +jinja2/tests.py,sha256=VLsBhVFnWg-PxSBz1MhRnNWgP1ovXk3neO1FLQMeC9Q,5926 +jinja2/utils.py,sha256=rRp3o9e7ZKS4fyrWRbELyLcpuGVTFcnooaOa1qx_FIk,24129 +jinja2/visitor.py,sha256=EcnL1PIwf_4RVCOMxsRNuR8AXHbS1qfAdMOE2ngKJz4,3557 diff --git a/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/WHEEL b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/WHEEL new file mode 100644 index 0000000..23d2d7e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/WHEEL @@ -0,0 +1,4 @@ +Wheel-Version: 1.0 +Generator: flit 3.11.0 +Root-Is-Purelib: true +Tag: py3-none-any diff --git a/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/entry_points.txt b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/entry_points.txt new file mode 100644 index 0000000..abc3eae --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[babel.extractors] +jinja2=jinja2.ext:babel_extract[i18n] + diff --git a/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..c37cae4 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt @@ -0,0 +1,28 @@ +Copyright 2007 Pallets + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + +1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A +PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED +TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/netdeploy/lib/python3.11/site-packages/jinja2/__init__.py b/netdeploy/lib/python3.11/site-packages/jinja2/__init__.py new file mode 100644 index 0000000..1a423a3 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2/__init__.py @@ -0,0 +1,38 @@ +"""Jinja is a template engine written in pure Python. It provides a +non-XML syntax that supports inline expressions and an optional +sandboxed environment. +""" + +from .bccache import BytecodeCache as BytecodeCache +from .bccache import FileSystemBytecodeCache as FileSystemBytecodeCache +from .bccache import MemcachedBytecodeCache as MemcachedBytecodeCache +from .environment import Environment as Environment +from .environment import Template as Template +from .exceptions import TemplateAssertionError as TemplateAssertionError +from .exceptions import TemplateError as TemplateError +from .exceptions import TemplateNotFound as TemplateNotFound +from .exceptions import TemplateRuntimeError as TemplateRuntimeError +from .exceptions import TemplatesNotFound as TemplatesNotFound +from .exceptions import TemplateSyntaxError as TemplateSyntaxError +from .exceptions import UndefinedError as UndefinedError +from .loaders import BaseLoader as BaseLoader +from .loaders import ChoiceLoader as ChoiceLoader +from .loaders import DictLoader as DictLoader +from .loaders import FileSystemLoader as FileSystemLoader +from .loaders import FunctionLoader as FunctionLoader +from .loaders import ModuleLoader as ModuleLoader +from .loaders import PackageLoader as PackageLoader +from .loaders import PrefixLoader as PrefixLoader +from .runtime import ChainableUndefined as ChainableUndefined +from .runtime import DebugUndefined as DebugUndefined +from .runtime import make_logging_undefined as make_logging_undefined +from .runtime import StrictUndefined as StrictUndefined +from .runtime import Undefined as Undefined +from .utils import clear_caches as clear_caches +from .utils import is_undefined as is_undefined +from .utils import pass_context as pass_context +from .utils import pass_environment as pass_environment +from .utils import pass_eval_context as pass_eval_context +from .utils import select_autoescape as select_autoescape + +__version__ = "3.1.6" diff --git a/netdeploy/lib/python3.11/site-packages/jinja2/_identifier.py b/netdeploy/lib/python3.11/site-packages/jinja2/_identifier.py new file mode 100644 index 0000000..928c150 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2/_identifier.py @@ -0,0 +1,6 @@ +import re + +# generated by scripts/generate_identifier_pattern.py +pattern = re.compile( + r"[\w·̀-ͯ·҃-֑҇-ׇֽֿׁׂׅׄؐ-ًؚ-ٰٟۖ-ۜ۟-۪ۤۧۨ-ܑۭܰ-݊ަ-ް߫-߽߳ࠖ-࠙ࠛ-ࠣࠥ-ࠧࠩ-࡙࠭-࡛࣓-ࣣ࣡-ःऺ-़ा-ॏ॑-ॗॢॣঁ-ঃ়া-ৄেৈো-্ৗৢৣ৾ਁ-ਃ਼ਾ-ੂੇੈੋ-੍ੑੰੱੵઁ-ઃ઼ા-ૅે-ૉો-્ૢૣૺ-૿ଁ-ଃ଼ା-ୄେୈୋ-୍ୖୗୢୣஂா-ூெ-ைொ-்ௗఀ-ఄా-ౄె-ైొ-్ౕౖౢౣಁ-ಃ಼ಾ-ೄೆ-ೈೊ-್ೕೖೢೣഀ-ഃ഻഼ാ-ൄെ-ൈൊ-്ൗൢൣංඃ්ා-ුූෘ-ෟෲෳัิ-ฺ็-๎ັິ-ູົຼ່-ໍ༹༘༙༵༷༾༿ཱ-྄྆྇ྍ-ྗྙ-ྼ࿆ါ-ှၖ-ၙၞ-ၠၢ-ၤၧ-ၭၱ-ၴႂ-ႍႏႚ-ႝ፝-፟ᜒ-᜔ᜲ-᜴ᝒᝓᝲᝳ឴-៓៝᠋-᠍ᢅᢆᢩᤠ-ᤫᤰ-᤻ᨗ-ᨛᩕ-ᩞ᩠-᩿᩼᪰-᪽ᬀ-ᬄ᬴-᭄᭫-᭳ᮀ-ᮂᮡ-ᮭ᯦-᯳ᰤ-᰷᳐-᳔᳒-᳨᳭ᳲ-᳴᳷-᳹᷀-᷹᷻-᷿‿⁀⁔⃐-⃥⃜⃡-⃰℘℮⳯-⵿⳱ⷠ-〪ⷿ-゙゚〯꙯ꙴ-꙽ꚞꚟ꛰꛱ꠂ꠆ꠋꠣ-ꠧꢀꢁꢴ-ꣅ꣠-꣱ꣿꤦ-꤭ꥇ-꥓ꦀ-ꦃ꦳-꧀ꧥꨩ-ꨶꩃꩌꩍꩻ-ꩽꪰꪲ-ꪴꪷꪸꪾ꪿꫁ꫫ-ꫯꫵ꫶ꯣ-ꯪ꯬꯭ﬞ︀-️︠-︯︳︴﹍-﹏_𐇽𐋠𐍶-𐍺𐨁-𐨃𐨅𐨆𐨌-𐨏𐨸-𐨿𐨺𐫦𐫥𐴤-𐽆𐴧-𐽐𑀀-𑀂𑀸-𑁆𑁿-𑂂𑂰-𑂺𑄀-𑄂𑄧-𑄴𑅅𑅆𑅳𑆀-𑆂𑆳-𑇀𑇉-𑇌𑈬-𑈷𑈾𑋟-𑋪𑌀-𑌃𑌻𑌼𑌾-𑍄𑍇𑍈𑍋-𑍍𑍗𑍢𑍣𑍦-𑍬𑍰-𑍴𑐵-𑑆𑑞𑒰-𑓃𑖯-𑖵𑖸-𑗀𑗜𑗝𑘰-𑙀𑚫-𑚷𑜝-𑜫𑠬-𑠺𑨁-𑨊𑨳-𑨹𑨻-𑨾𑩇𑩑-𑩛𑪊-𑪙𑰯-𑰶𑰸-𑰿𑲒-𑲧𑲩-𑲶𑴱-𑴶𑴺𑴼𑴽𑴿-𑵅𑵇𑶊-𑶎𑶐𑶑𑶓-𑶗𑻳-𑻶𖫰-𖫴𖬰-𖬶𖽑-𖽾𖾏-𖾒𛲝𛲞𝅥-𝅩𝅭-𝅲𝅻-𝆂𝆅-𝆋𝆪-𝆭𝉂-𝉄𝨀-𝨶𝨻-𝩬𝩵𝪄𝪛-𝪟𝪡-𝪯𞀀-𞀆𞀈-𞀘𞀛-𞀡𞀣𞀤𞀦-𞣐𞀪-𞣖𞥄-𞥊󠄀-󠇯]+" # noqa: B950 +) diff --git a/netdeploy/lib/python3.11/site-packages/jinja2/async_utils.py b/netdeploy/lib/python3.11/site-packages/jinja2/async_utils.py new file mode 100644 index 0000000..f0c1402 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2/async_utils.py @@ -0,0 +1,99 @@ +import inspect +import typing as t +from functools import WRAPPER_ASSIGNMENTS +from functools import wraps + +from .utils import _PassArg +from .utils import pass_eval_context + +if t.TYPE_CHECKING: + import typing_extensions as te + +V = t.TypeVar("V") + + +def async_variant(normal_func): # type: ignore + def decorator(async_func): # type: ignore + pass_arg = _PassArg.from_obj(normal_func) + need_eval_context = pass_arg is None + + if pass_arg is _PassArg.environment: + + def is_async(args: t.Any) -> bool: + return t.cast(bool, args[0].is_async) + + else: + + def is_async(args: t.Any) -> bool: + return t.cast(bool, args[0].environment.is_async) + + # Take the doc and annotations from the sync function, but the + # name from the async function. Pallets-Sphinx-Themes + # build_function_directive expects __wrapped__ to point to the + # sync function. + async_func_attrs = ("__module__", "__name__", "__qualname__") + normal_func_attrs = tuple(set(WRAPPER_ASSIGNMENTS).difference(async_func_attrs)) + + @wraps(normal_func, assigned=normal_func_attrs) + @wraps(async_func, assigned=async_func_attrs, updated=()) + def wrapper(*args, **kwargs): # type: ignore + b = is_async(args) + + if need_eval_context: + args = args[1:] + + if b: + return async_func(*args, **kwargs) + + return normal_func(*args, **kwargs) + + if need_eval_context: + wrapper = pass_eval_context(wrapper) + + wrapper.jinja_async_variant = True # type: ignore[attr-defined] + return wrapper + + return decorator + + +_common_primitives = {int, float, bool, str, list, dict, tuple, type(None)} + + +async def auto_await(value: t.Union[t.Awaitable["V"], "V"]) -> "V": + # Avoid a costly call to isawaitable + if type(value) in _common_primitives: + return t.cast("V", value) + + if inspect.isawaitable(value): + return await t.cast("t.Awaitable[V]", value) + + return value + + +class _IteratorToAsyncIterator(t.Generic[V]): + def __init__(self, iterator: "t.Iterator[V]"): + self._iterator = iterator + + def __aiter__(self) -> "te.Self": + return self + + async def __anext__(self) -> V: + try: + return next(self._iterator) + except StopIteration as e: + raise StopAsyncIteration(e.value) from e + + +def auto_aiter( + iterable: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", +) -> "t.AsyncIterator[V]": + if hasattr(iterable, "__aiter__"): + return iterable.__aiter__() + else: + return _IteratorToAsyncIterator(iter(iterable)) + + +async def auto_to_list( + value: "t.Union[t.AsyncIterable[V], t.Iterable[V]]", +) -> t.List["V"]: + return [x async for x in auto_aiter(value)] diff --git a/netdeploy/lib/python3.11/site-packages/jinja2/bccache.py b/netdeploy/lib/python3.11/site-packages/jinja2/bccache.py new file mode 100644 index 0000000..ada8b09 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2/bccache.py @@ -0,0 +1,408 @@ +"""The optional bytecode cache system. This is useful if you have very +complex template situations and the compilation of all those templates +slows down your application too much. + +Situations where this is useful are often forking web applications that +are initialized on the first request. +""" + +import errno +import fnmatch +import marshal +import os +import pickle +import stat +import sys +import tempfile +import typing as t +from hashlib import sha1 +from io import BytesIO +from types import CodeType + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .environment import Environment + + class _MemcachedClient(te.Protocol): + def get(self, key: str) -> bytes: ... + + def set( + self, key: str, value: bytes, timeout: t.Optional[int] = None + ) -> None: ... + + +bc_version = 5 +# Magic bytes to identify Jinja bytecode cache files. Contains the +# Python major and minor version to avoid loading incompatible bytecode +# if a project upgrades its Python version. +bc_magic = ( + b"j2" + + pickle.dumps(bc_version, 2) + + pickle.dumps((sys.version_info[0] << 24) | sys.version_info[1], 2) +) + + +class Bucket: + """Buckets are used to store the bytecode for one template. It's created + and initialized by the bytecode cache and passed to the loading functions. + + The buckets get an internal checksum from the cache assigned and use this + to automatically reject outdated cache material. Individual bytecode + cache subclasses don't have to care about cache invalidation. + """ + + def __init__(self, environment: "Environment", key: str, checksum: str) -> None: + self.environment = environment + self.key = key + self.checksum = checksum + self.reset() + + def reset(self) -> None: + """Resets the bucket (unloads the bytecode).""" + self.code: t.Optional[CodeType] = None + + def load_bytecode(self, f: t.BinaryIO) -> None: + """Loads bytecode from a file or file like object.""" + # make sure the magic header is correct + magic = f.read(len(bc_magic)) + if magic != bc_magic: + self.reset() + return + # the source code of the file changed, we need to reload + checksum = pickle.load(f) + if self.checksum != checksum: + self.reset() + return + # if marshal_load fails then we need to reload + try: + self.code = marshal.load(f) + except (EOFError, ValueError, TypeError): + self.reset() + return + + def write_bytecode(self, f: t.IO[bytes]) -> None: + """Dump the bytecode into the file or file like object passed.""" + if self.code is None: + raise TypeError("can't write empty bucket") + f.write(bc_magic) + pickle.dump(self.checksum, f, 2) + marshal.dump(self.code, f) + + def bytecode_from_string(self, string: bytes) -> None: + """Load bytecode from bytes.""" + self.load_bytecode(BytesIO(string)) + + def bytecode_to_string(self) -> bytes: + """Return the bytecode as bytes.""" + out = BytesIO() + self.write_bytecode(out) + return out.getvalue() + + +class BytecodeCache: + """To implement your own bytecode cache you have to subclass this class + and override :meth:`load_bytecode` and :meth:`dump_bytecode`. Both of + these methods are passed a :class:`~jinja2.bccache.Bucket`. + + A very basic bytecode cache that saves the bytecode on the file system:: + + from os import path + + class MyCache(BytecodeCache): + + def __init__(self, directory): + self.directory = directory + + def load_bytecode(self, bucket): + filename = path.join(self.directory, bucket.key) + if path.exists(filename): + with open(filename, 'rb') as f: + bucket.load_bytecode(f) + + def dump_bytecode(self, bucket): + filename = path.join(self.directory, bucket.key) + with open(filename, 'wb') as f: + bucket.write_bytecode(f) + + A more advanced version of a filesystem based bytecode cache is part of + Jinja. + """ + + def load_bytecode(self, bucket: Bucket) -> None: + """Subclasses have to override this method to load bytecode into a + bucket. If they are not able to find code in the cache for the + bucket, it must not do anything. + """ + raise NotImplementedError() + + def dump_bytecode(self, bucket: Bucket) -> None: + """Subclasses have to override this method to write the bytecode + from a bucket back to the cache. If it unable to do so it must not + fail silently but raise an exception. + """ + raise NotImplementedError() + + def clear(self) -> None: + """Clears the cache. This method is not used by Jinja but should be + implemented to allow applications to clear the bytecode cache used + by a particular environment. + """ + + def get_cache_key( + self, name: str, filename: t.Optional[t.Union[str]] = None + ) -> str: + """Returns the unique hash key for this template name.""" + hash = sha1(name.encode("utf-8")) + + if filename is not None: + hash.update(f"|{filename}".encode()) + + return hash.hexdigest() + + def get_source_checksum(self, source: str) -> str: + """Returns a checksum for the source.""" + return sha1(source.encode("utf-8")).hexdigest() + + def get_bucket( + self, + environment: "Environment", + name: str, + filename: t.Optional[str], + source: str, + ) -> Bucket: + """Return a cache bucket for the given template. All arguments are + mandatory but filename may be `None`. + """ + key = self.get_cache_key(name, filename) + checksum = self.get_source_checksum(source) + bucket = Bucket(environment, key, checksum) + self.load_bytecode(bucket) + return bucket + + def set_bucket(self, bucket: Bucket) -> None: + """Put the bucket into the cache.""" + self.dump_bytecode(bucket) + + +class FileSystemBytecodeCache(BytecodeCache): + """A bytecode cache that stores bytecode on the filesystem. It accepts + two arguments: The directory where the cache items are stored and a + pattern string that is used to build the filename. + + If no directory is specified a default cache directory is selected. On + Windows the user's temp directory is used, on UNIX systems a directory + is created for the user in the system temp directory. + + The pattern can be used to have multiple separate caches operate on the + same directory. The default pattern is ``'__jinja2_%s.cache'``. ``%s`` + is replaced with the cache key. + + >>> bcc = FileSystemBytecodeCache('/tmp/jinja_cache', '%s.cache') + + This bytecode cache supports clearing of the cache using the clear method. + """ + + def __init__( + self, directory: t.Optional[str] = None, pattern: str = "__jinja2_%s.cache" + ) -> None: + if directory is None: + directory = self._get_default_cache_dir() + self.directory = directory + self.pattern = pattern + + def _get_default_cache_dir(self) -> str: + def _unsafe_dir() -> "te.NoReturn": + raise RuntimeError( + "Cannot determine safe temp directory. You " + "need to explicitly provide one." + ) + + tmpdir = tempfile.gettempdir() + + # On windows the temporary directory is used specific unless + # explicitly forced otherwise. We can just use that. + if os.name == "nt": + return tmpdir + if not hasattr(os, "getuid"): + _unsafe_dir() + + dirname = f"_jinja2-cache-{os.getuid()}" + actual_dir = os.path.join(tmpdir, dirname) + + try: + os.mkdir(actual_dir, stat.S_IRWXU) + except OSError as e: + if e.errno != errno.EEXIST: + raise + try: + os.chmod(actual_dir, stat.S_IRWXU) + actual_dir_stat = os.lstat(actual_dir) + if ( + actual_dir_stat.st_uid != os.getuid() + or not stat.S_ISDIR(actual_dir_stat.st_mode) + or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU + ): + _unsafe_dir() + except OSError as e: + if e.errno != errno.EEXIST: + raise + + actual_dir_stat = os.lstat(actual_dir) + if ( + actual_dir_stat.st_uid != os.getuid() + or not stat.S_ISDIR(actual_dir_stat.st_mode) + or stat.S_IMODE(actual_dir_stat.st_mode) != stat.S_IRWXU + ): + _unsafe_dir() + + return actual_dir + + def _get_cache_filename(self, bucket: Bucket) -> str: + return os.path.join(self.directory, self.pattern % (bucket.key,)) + + def load_bytecode(self, bucket: Bucket) -> None: + filename = self._get_cache_filename(bucket) + + # Don't test for existence before opening the file, since the + # file could disappear after the test before the open. + try: + f = open(filename, "rb") + except (FileNotFoundError, IsADirectoryError, PermissionError): + # PermissionError can occur on Windows when an operation is + # in progress, such as calling clear(). + return + + with f: + bucket.load_bytecode(f) + + def dump_bytecode(self, bucket: Bucket) -> None: + # Write to a temporary file, then rename to the real name after + # writing. This avoids another process reading the file before + # it is fully written. + name = self._get_cache_filename(bucket) + f = tempfile.NamedTemporaryFile( + mode="wb", + dir=os.path.dirname(name), + prefix=os.path.basename(name), + suffix=".tmp", + delete=False, + ) + + def remove_silent() -> None: + try: + os.remove(f.name) + except OSError: + # Another process may have called clear(). On Windows, + # another program may be holding the file open. + pass + + try: + with f: + bucket.write_bytecode(f) + except BaseException: + remove_silent() + raise + + try: + os.replace(f.name, name) + except OSError: + # Another process may have called clear(). On Windows, + # another program may be holding the file open. + remove_silent() + except BaseException: + remove_silent() + raise + + def clear(self) -> None: + # imported lazily here because google app-engine doesn't support + # write access on the file system and the function does not exist + # normally. + from os import remove + + files = fnmatch.filter(os.listdir(self.directory), self.pattern % ("*",)) + for filename in files: + try: + remove(os.path.join(self.directory, filename)) + except OSError: + pass + + +class MemcachedBytecodeCache(BytecodeCache): + """This class implements a bytecode cache that uses a memcache cache for + storing the information. It does not enforce a specific memcache library + (tummy's memcache or cmemcache) but will accept any class that provides + the minimal interface required. + + Libraries compatible with this class: + + - `cachelib `_ + - `python-memcached `_ + + (Unfortunately the django cache interface is not compatible because it + does not support storing binary data, only text. You can however pass + the underlying cache client to the bytecode cache which is available + as `django.core.cache.cache._client`.) + + The minimal interface for the client passed to the constructor is this: + + .. class:: MinimalClientInterface + + .. method:: set(key, value[, timeout]) + + Stores the bytecode in the cache. `value` is a string and + `timeout` the timeout of the key. If timeout is not provided + a default timeout or no timeout should be assumed, if it's + provided it's an integer with the number of seconds the cache + item should exist. + + .. method:: get(key) + + Returns the value for the cache key. If the item does not + exist in the cache the return value must be `None`. + + The other arguments to the constructor are the prefix for all keys that + is added before the actual cache key and the timeout for the bytecode in + the cache system. We recommend a high (or no) timeout. + + This bytecode cache does not support clearing of used items in the cache. + The clear method is a no-operation function. + + .. versionadded:: 2.7 + Added support for ignoring memcache errors through the + `ignore_memcache_errors` parameter. + """ + + def __init__( + self, + client: "_MemcachedClient", + prefix: str = "jinja2/bytecode/", + timeout: t.Optional[int] = None, + ignore_memcache_errors: bool = True, + ): + self.client = client + self.prefix = prefix + self.timeout = timeout + self.ignore_memcache_errors = ignore_memcache_errors + + def load_bytecode(self, bucket: Bucket) -> None: + try: + code = self.client.get(self.prefix + bucket.key) + except Exception: + if not self.ignore_memcache_errors: + raise + else: + bucket.bytecode_from_string(code) + + def dump_bytecode(self, bucket: Bucket) -> None: + key = self.prefix + bucket.key + value = bucket.bytecode_to_string() + + try: + if self.timeout is not None: + self.client.set(key, value, self.timeout) + else: + self.client.set(key, value) + except Exception: + if not self.ignore_memcache_errors: + raise diff --git a/netdeploy/lib/python3.11/site-packages/jinja2/compiler.py b/netdeploy/lib/python3.11/site-packages/jinja2/compiler.py new file mode 100644 index 0000000..a4ff6a1 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2/compiler.py @@ -0,0 +1,1998 @@ +"""Compiles nodes from the parser into Python code.""" + +import typing as t +from contextlib import contextmanager +from functools import update_wrapper +from io import StringIO +from itertools import chain +from keyword import iskeyword as is_python_keyword + +from markupsafe import escape +from markupsafe import Markup + +from . import nodes +from .exceptions import TemplateAssertionError +from .idtracking import Symbols +from .idtracking import VAR_LOAD_ALIAS +from .idtracking import VAR_LOAD_PARAMETER +from .idtracking import VAR_LOAD_RESOLVE +from .idtracking import VAR_LOAD_UNDEFINED +from .nodes import EvalContext +from .optimizer import Optimizer +from .utils import _PassArg +from .utils import concat +from .visitor import NodeVisitor + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .environment import Environment + +F = t.TypeVar("F", bound=t.Callable[..., t.Any]) + +operators = { + "eq": "==", + "ne": "!=", + "gt": ">", + "gteq": ">=", + "lt": "<", + "lteq": "<=", + "in": "in", + "notin": "not in", +} + + +def optimizeconst(f: F) -> F: + def new_func( + self: "CodeGenerator", node: nodes.Expr, frame: "Frame", **kwargs: t.Any + ) -> t.Any: + # Only optimize if the frame is not volatile + if self.optimizer is not None and not frame.eval_ctx.volatile: + new_node = self.optimizer.visit(node, frame.eval_ctx) + + if new_node != node: + return self.visit(new_node, frame) + + return f(self, node, frame, **kwargs) + + return update_wrapper(new_func, f) # type: ignore[return-value] + + +def _make_binop(op: str) -> t.Callable[["CodeGenerator", nodes.BinExpr, "Frame"], None]: + @optimizeconst + def visitor(self: "CodeGenerator", node: nodes.BinExpr, frame: Frame) -> None: + if ( + self.environment.sandboxed and op in self.environment.intercepted_binops # type: ignore + ): + self.write(f"environment.call_binop(context, {op!r}, ") + self.visit(node.left, frame) + self.write(", ") + self.visit(node.right, frame) + else: + self.write("(") + self.visit(node.left, frame) + self.write(f" {op} ") + self.visit(node.right, frame) + + self.write(")") + + return visitor + + +def _make_unop( + op: str, +) -> t.Callable[["CodeGenerator", nodes.UnaryExpr, "Frame"], None]: + @optimizeconst + def visitor(self: "CodeGenerator", node: nodes.UnaryExpr, frame: Frame) -> None: + if ( + self.environment.sandboxed and op in self.environment.intercepted_unops # type: ignore + ): + self.write(f"environment.call_unop(context, {op!r}, ") + self.visit(node.node, frame) + else: + self.write("(" + op) + self.visit(node.node, frame) + + self.write(")") + + return visitor + + +def generate( + node: nodes.Template, + environment: "Environment", + name: t.Optional[str], + filename: t.Optional[str], + stream: t.Optional[t.TextIO] = None, + defer_init: bool = False, + optimized: bool = True, +) -> t.Optional[str]: + """Generate the python source for a node tree.""" + if not isinstance(node, nodes.Template): + raise TypeError("Can't compile non template nodes") + + generator = environment.code_generator_class( + environment, name, filename, stream, defer_init, optimized + ) + generator.visit(node) + + if stream is None: + return generator.stream.getvalue() # type: ignore + + return None + + +def has_safe_repr(value: t.Any) -> bool: + """Does the node have a safe representation?""" + if value is None or value is NotImplemented or value is Ellipsis: + return True + + if type(value) in {bool, int, float, complex, range, str, Markup}: + return True + + if type(value) in {tuple, list, set, frozenset}: + return all(has_safe_repr(v) for v in value) + + if type(value) is dict: # noqa E721 + return all(has_safe_repr(k) and has_safe_repr(v) for k, v in value.items()) + + return False + + +def find_undeclared( + nodes: t.Iterable[nodes.Node], names: t.Iterable[str] +) -> t.Set[str]: + """Check if the names passed are accessed undeclared. The return value + is a set of all the undeclared names from the sequence of names found. + """ + visitor = UndeclaredNameVisitor(names) + try: + for node in nodes: + visitor.visit(node) + except VisitorExit: + pass + return visitor.undeclared + + +class MacroRef: + def __init__(self, node: t.Union[nodes.Macro, nodes.CallBlock]) -> None: + self.node = node + self.accesses_caller = False + self.accesses_kwargs = False + self.accesses_varargs = False + + +class Frame: + """Holds compile time information for us.""" + + def __init__( + self, + eval_ctx: EvalContext, + parent: t.Optional["Frame"] = None, + level: t.Optional[int] = None, + ) -> None: + self.eval_ctx = eval_ctx + + # the parent of this frame + self.parent = parent + + if parent is None: + self.symbols = Symbols(level=level) + + # in some dynamic inheritance situations the compiler needs to add + # write tests around output statements. + self.require_output_check = False + + # inside some tags we are using a buffer rather than yield statements. + # this for example affects {% filter %} or {% macro %}. If a frame + # is buffered this variable points to the name of the list used as + # buffer. + self.buffer: t.Optional[str] = None + + # the name of the block we're in, otherwise None. + self.block: t.Optional[str] = None + + else: + self.symbols = Symbols(parent.symbols, level=level) + self.require_output_check = parent.require_output_check + self.buffer = parent.buffer + self.block = parent.block + + # a toplevel frame is the root + soft frames such as if conditions. + self.toplevel = False + + # the root frame is basically just the outermost frame, so no if + # conditions. This information is used to optimize inheritance + # situations. + self.rootlevel = False + + # variables set inside of loops and blocks should not affect outer frames, + # but they still needs to be kept track of as part of the active context. + self.loop_frame = False + self.block_frame = False + + # track whether the frame is being used in an if-statement or conditional + # expression as it determines which errors should be raised during runtime + # or compile time. + self.soft_frame = False + + def copy(self) -> "te.Self": + """Create a copy of the current one.""" + rv = object.__new__(self.__class__) + rv.__dict__.update(self.__dict__) + rv.symbols = self.symbols.copy() + return rv + + def inner(self, isolated: bool = False) -> "Frame": + """Return an inner frame.""" + if isolated: + return Frame(self.eval_ctx, level=self.symbols.level + 1) + return Frame(self.eval_ctx, self) + + def soft(self) -> "te.Self": + """Return a soft frame. A soft frame may not be modified as + standalone thing as it shares the resources with the frame it + was created of, but it's not a rootlevel frame any longer. + + This is only used to implement if-statements and conditional + expressions. + """ + rv = self.copy() + rv.rootlevel = False + rv.soft_frame = True + return rv + + __copy__ = copy + + +class VisitorExit(RuntimeError): + """Exception used by the `UndeclaredNameVisitor` to signal a stop.""" + + +class DependencyFinderVisitor(NodeVisitor): + """A visitor that collects filter and test calls.""" + + def __init__(self) -> None: + self.filters: t.Set[str] = set() + self.tests: t.Set[str] = set() + + def visit_Filter(self, node: nodes.Filter) -> None: + self.generic_visit(node) + self.filters.add(node.name) + + def visit_Test(self, node: nodes.Test) -> None: + self.generic_visit(node) + self.tests.add(node.name) + + def visit_Block(self, node: nodes.Block) -> None: + """Stop visiting at blocks.""" + + +class UndeclaredNameVisitor(NodeVisitor): + """A visitor that checks if a name is accessed without being + declared. This is different from the frame visitor as it will + not stop at closure frames. + """ + + def __init__(self, names: t.Iterable[str]) -> None: + self.names = set(names) + self.undeclared: t.Set[str] = set() + + def visit_Name(self, node: nodes.Name) -> None: + if node.ctx == "load" and node.name in self.names: + self.undeclared.add(node.name) + if self.undeclared == self.names: + raise VisitorExit() + else: + self.names.discard(node.name) + + def visit_Block(self, node: nodes.Block) -> None: + """Stop visiting a blocks.""" + + +class CompilerExit(Exception): + """Raised if the compiler encountered a situation where it just + doesn't make sense to further process the code. Any block that + raises such an exception is not further processed. + """ + + +class CodeGenerator(NodeVisitor): + def __init__( + self, + environment: "Environment", + name: t.Optional[str], + filename: t.Optional[str], + stream: t.Optional[t.TextIO] = None, + defer_init: bool = False, + optimized: bool = True, + ) -> None: + if stream is None: + stream = StringIO() + self.environment = environment + self.name = name + self.filename = filename + self.stream = stream + self.created_block_context = False + self.defer_init = defer_init + self.optimizer: t.Optional[Optimizer] = None + + if optimized: + self.optimizer = Optimizer(environment) + + # aliases for imports + self.import_aliases: t.Dict[str, str] = {} + + # a registry for all blocks. Because blocks are moved out + # into the global python scope they are registered here + self.blocks: t.Dict[str, nodes.Block] = {} + + # the number of extends statements so far + self.extends_so_far = 0 + + # some templates have a rootlevel extends. In this case we + # can safely assume that we're a child template and do some + # more optimizations. + self.has_known_extends = False + + # the current line number + self.code_lineno = 1 + + # registry of all filters and tests (global, not block local) + self.tests: t.Dict[str, str] = {} + self.filters: t.Dict[str, str] = {} + + # the debug information + self.debug_info: t.List[t.Tuple[int, int]] = [] + self._write_debug_info: t.Optional[int] = None + + # the number of new lines before the next write() + self._new_lines = 0 + + # the line number of the last written statement + self._last_line = 0 + + # true if nothing was written so far. + self._first_write = True + + # used by the `temporary_identifier` method to get new + # unique, temporary identifier + self._last_identifier = 0 + + # the current indentation + self._indentation = 0 + + # Tracks toplevel assignments + self._assign_stack: t.List[t.Set[str]] = [] + + # Tracks parameter definition blocks + self._param_def_block: t.List[t.Set[str]] = [] + + # Tracks the current context. + self._context_reference_stack = ["context"] + + @property + def optimized(self) -> bool: + return self.optimizer is not None + + # -- Various compilation helpers + + def fail(self, msg: str, lineno: int) -> "te.NoReturn": + """Fail with a :exc:`TemplateAssertionError`.""" + raise TemplateAssertionError(msg, lineno, self.name, self.filename) + + def temporary_identifier(self) -> str: + """Get a new unique identifier.""" + self._last_identifier += 1 + return f"t_{self._last_identifier}" + + def buffer(self, frame: Frame) -> None: + """Enable buffering for the frame from that point onwards.""" + frame.buffer = self.temporary_identifier() + self.writeline(f"{frame.buffer} = []") + + def return_buffer_contents( + self, frame: Frame, force_unescaped: bool = False + ) -> None: + """Return the buffer contents of the frame.""" + if not force_unescaped: + if frame.eval_ctx.volatile: + self.writeline("if context.eval_ctx.autoescape:") + self.indent() + self.writeline(f"return Markup(concat({frame.buffer}))") + self.outdent() + self.writeline("else:") + self.indent() + self.writeline(f"return concat({frame.buffer})") + self.outdent() + return + elif frame.eval_ctx.autoescape: + self.writeline(f"return Markup(concat({frame.buffer}))") + return + self.writeline(f"return concat({frame.buffer})") + + def indent(self) -> None: + """Indent by one.""" + self._indentation += 1 + + def outdent(self, step: int = 1) -> None: + """Outdent by step.""" + self._indentation -= step + + def start_write(self, frame: Frame, node: t.Optional[nodes.Node] = None) -> None: + """Yield or write into the frame buffer.""" + if frame.buffer is None: + self.writeline("yield ", node) + else: + self.writeline(f"{frame.buffer}.append(", node) + + def end_write(self, frame: Frame) -> None: + """End the writing process started by `start_write`.""" + if frame.buffer is not None: + self.write(")") + + def simple_write( + self, s: str, frame: Frame, node: t.Optional[nodes.Node] = None + ) -> None: + """Simple shortcut for start_write + write + end_write.""" + self.start_write(frame, node) + self.write(s) + self.end_write(frame) + + def blockvisit(self, nodes: t.Iterable[nodes.Node], frame: Frame) -> None: + """Visit a list of nodes as block in a frame. If the current frame + is no buffer a dummy ``if 0: yield None`` is written automatically. + """ + try: + self.writeline("pass") + for node in nodes: + self.visit(node, frame) + except CompilerExit: + pass + + def write(self, x: str) -> None: + """Write a string into the output stream.""" + if self._new_lines: + if not self._first_write: + self.stream.write("\n" * self._new_lines) + self.code_lineno += self._new_lines + if self._write_debug_info is not None: + self.debug_info.append((self._write_debug_info, self.code_lineno)) + self._write_debug_info = None + self._first_write = False + self.stream.write(" " * self._indentation) + self._new_lines = 0 + self.stream.write(x) + + def writeline( + self, x: str, node: t.Optional[nodes.Node] = None, extra: int = 0 + ) -> None: + """Combination of newline and write.""" + self.newline(node, extra) + self.write(x) + + def newline(self, node: t.Optional[nodes.Node] = None, extra: int = 0) -> None: + """Add one or more newlines before the next write.""" + self._new_lines = max(self._new_lines, 1 + extra) + if node is not None and node.lineno != self._last_line: + self._write_debug_info = node.lineno + self._last_line = node.lineno + + def signature( + self, + node: t.Union[nodes.Call, nodes.Filter, nodes.Test], + frame: Frame, + extra_kwargs: t.Optional[t.Mapping[str, t.Any]] = None, + ) -> None: + """Writes a function call to the stream for the current node. + A leading comma is added automatically. The extra keyword + arguments may not include python keywords otherwise a syntax + error could occur. The extra keyword arguments should be given + as python dict. + """ + # if any of the given keyword arguments is a python keyword + # we have to make sure that no invalid call is created. + kwarg_workaround = any( + is_python_keyword(t.cast(str, k)) + for k in chain((x.key for x in node.kwargs), extra_kwargs or ()) + ) + + for arg in node.args: + self.write(", ") + self.visit(arg, frame) + + if not kwarg_workaround: + for kwarg in node.kwargs: + self.write(", ") + self.visit(kwarg, frame) + if extra_kwargs is not None: + for key, value in extra_kwargs.items(): + self.write(f", {key}={value}") + if node.dyn_args: + self.write(", *") + self.visit(node.dyn_args, frame) + + if kwarg_workaround: + if node.dyn_kwargs is not None: + self.write(", **dict({") + else: + self.write(", **{") + for kwarg in node.kwargs: + self.write(f"{kwarg.key!r}: ") + self.visit(kwarg.value, frame) + self.write(", ") + if extra_kwargs is not None: + for key, value in extra_kwargs.items(): + self.write(f"{key!r}: {value}, ") + if node.dyn_kwargs is not None: + self.write("}, **") + self.visit(node.dyn_kwargs, frame) + self.write(")") + else: + self.write("}") + + elif node.dyn_kwargs is not None: + self.write(", **") + self.visit(node.dyn_kwargs, frame) + + def pull_dependencies(self, nodes: t.Iterable[nodes.Node]) -> None: + """Find all filter and test names used in the template and + assign them to variables in the compiled namespace. Checking + that the names are registered with the environment is done when + compiling the Filter and Test nodes. If the node is in an If or + CondExpr node, the check is done at runtime instead. + + .. versionchanged:: 3.0 + Filters and tests in If and CondExpr nodes are checked at + runtime instead of compile time. + """ + visitor = DependencyFinderVisitor() + + for node in nodes: + visitor.visit(node) + + for id_map, names, dependency in ( + (self.filters, visitor.filters, "filters"), + ( + self.tests, + visitor.tests, + "tests", + ), + ): + for name in sorted(names): + if name not in id_map: + id_map[name] = self.temporary_identifier() + + # add check during runtime that dependencies used inside of executed + # blocks are defined, as this step may be skipped during compile time + self.writeline("try:") + self.indent() + self.writeline(f"{id_map[name]} = environment.{dependency}[{name!r}]") + self.outdent() + self.writeline("except KeyError:") + self.indent() + self.writeline("@internalcode") + self.writeline(f"def {id_map[name]}(*unused):") + self.indent() + self.writeline( + f'raise TemplateRuntimeError("No {dependency[:-1]}' + f' named {name!r} found.")' + ) + self.outdent() + self.outdent() + + def enter_frame(self, frame: Frame) -> None: + undefs = [] + for target, (action, param) in frame.symbols.loads.items(): + if action == VAR_LOAD_PARAMETER: + pass + elif action == VAR_LOAD_RESOLVE: + self.writeline(f"{target} = {self.get_resolve_func()}({param!r})") + elif action == VAR_LOAD_ALIAS: + self.writeline(f"{target} = {param}") + elif action == VAR_LOAD_UNDEFINED: + undefs.append(target) + else: + raise NotImplementedError("unknown load instruction") + if undefs: + self.writeline(f"{' = '.join(undefs)} = missing") + + def leave_frame(self, frame: Frame, with_python_scope: bool = False) -> None: + if not with_python_scope: + undefs = [] + for target in frame.symbols.loads: + undefs.append(target) + if undefs: + self.writeline(f"{' = '.join(undefs)} = missing") + + def choose_async(self, async_value: str = "async ", sync_value: str = "") -> str: + return async_value if self.environment.is_async else sync_value + + def func(self, name: str) -> str: + return f"{self.choose_async()}def {name}" + + def macro_body( + self, node: t.Union[nodes.Macro, nodes.CallBlock], frame: Frame + ) -> t.Tuple[Frame, MacroRef]: + """Dump the function def of a macro or call block.""" + frame = frame.inner() + frame.symbols.analyze_node(node) + macro_ref = MacroRef(node) + + explicit_caller = None + skip_special_params = set() + args = [] + + for idx, arg in enumerate(node.args): + if arg.name == "caller": + explicit_caller = idx + if arg.name in ("kwargs", "varargs"): + skip_special_params.add(arg.name) + args.append(frame.symbols.ref(arg.name)) + + undeclared = find_undeclared(node.body, ("caller", "kwargs", "varargs")) + + if "caller" in undeclared: + # In older Jinja versions there was a bug that allowed caller + # to retain the special behavior even if it was mentioned in + # the argument list. However thankfully this was only really + # working if it was the last argument. So we are explicitly + # checking this now and error out if it is anywhere else in + # the argument list. + if explicit_caller is not None: + try: + node.defaults[explicit_caller - len(node.args)] + except IndexError: + self.fail( + "When defining macros or call blocks the " + 'special "caller" argument must be omitted ' + "or be given a default.", + node.lineno, + ) + else: + args.append(frame.symbols.declare_parameter("caller")) + macro_ref.accesses_caller = True + if "kwargs" in undeclared and "kwargs" not in skip_special_params: + args.append(frame.symbols.declare_parameter("kwargs")) + macro_ref.accesses_kwargs = True + if "varargs" in undeclared and "varargs" not in skip_special_params: + args.append(frame.symbols.declare_parameter("varargs")) + macro_ref.accesses_varargs = True + + # macros are delayed, they never require output checks + frame.require_output_check = False + frame.symbols.analyze_node(node) + self.writeline(f"{self.func('macro')}({', '.join(args)}):", node) + self.indent() + + self.buffer(frame) + self.enter_frame(frame) + + self.push_parameter_definitions(frame) + for idx, arg in enumerate(node.args): + ref = frame.symbols.ref(arg.name) + self.writeline(f"if {ref} is missing:") + self.indent() + try: + default = node.defaults[idx - len(node.args)] + except IndexError: + self.writeline( + f'{ref} = undefined("parameter {arg.name!r} was not provided",' + f" name={arg.name!r})" + ) + else: + self.writeline(f"{ref} = ") + self.visit(default, frame) + self.mark_parameter_stored(ref) + self.outdent() + self.pop_parameter_definitions() + + self.blockvisit(node.body, frame) + self.return_buffer_contents(frame, force_unescaped=True) + self.leave_frame(frame, with_python_scope=True) + self.outdent() + + return frame, macro_ref + + def macro_def(self, macro_ref: MacroRef, frame: Frame) -> None: + """Dump the macro definition for the def created by macro_body.""" + arg_tuple = ", ".join(repr(x.name) for x in macro_ref.node.args) + name = getattr(macro_ref.node, "name", None) + if len(macro_ref.node.args) == 1: + arg_tuple += "," + self.write( + f"Macro(environment, macro, {name!r}, ({arg_tuple})," + f" {macro_ref.accesses_kwargs!r}, {macro_ref.accesses_varargs!r}," + f" {macro_ref.accesses_caller!r}, context.eval_ctx.autoescape)" + ) + + def position(self, node: nodes.Node) -> str: + """Return a human readable position for the node.""" + rv = f"line {node.lineno}" + if self.name is not None: + rv = f"{rv} in {self.name!r}" + return rv + + def dump_local_context(self, frame: Frame) -> str: + items_kv = ", ".join( + f"{name!r}: {target}" + for name, target in frame.symbols.dump_stores().items() + ) + return f"{{{items_kv}}}" + + def write_commons(self) -> None: + """Writes a common preamble that is used by root and block functions. + Primarily this sets up common local helpers and enforces a generator + through a dead branch. + """ + self.writeline("resolve = context.resolve_or_missing") + self.writeline("undefined = environment.undefined") + self.writeline("concat = environment.concat") + # always use the standard Undefined class for the implicit else of + # conditional expressions + self.writeline("cond_expr_undefined = Undefined") + self.writeline("if 0: yield None") + + def push_parameter_definitions(self, frame: Frame) -> None: + """Pushes all parameter targets from the given frame into a local + stack that permits tracking of yet to be assigned parameters. In + particular this enables the optimization from `visit_Name` to skip + undefined expressions for parameters in macros as macros can reference + otherwise unbound parameters. + """ + self._param_def_block.append(frame.symbols.dump_param_targets()) + + def pop_parameter_definitions(self) -> None: + """Pops the current parameter definitions set.""" + self._param_def_block.pop() + + def mark_parameter_stored(self, target: str) -> None: + """Marks a parameter in the current parameter definitions as stored. + This will skip the enforced undefined checks. + """ + if self._param_def_block: + self._param_def_block[-1].discard(target) + + def push_context_reference(self, target: str) -> None: + self._context_reference_stack.append(target) + + def pop_context_reference(self) -> None: + self._context_reference_stack.pop() + + def get_context_ref(self) -> str: + return self._context_reference_stack[-1] + + def get_resolve_func(self) -> str: + target = self._context_reference_stack[-1] + if target == "context": + return "resolve" + return f"{target}.resolve" + + def derive_context(self, frame: Frame) -> str: + return f"{self.get_context_ref()}.derived({self.dump_local_context(frame)})" + + def parameter_is_undeclared(self, target: str) -> bool: + """Checks if a given target is an undeclared parameter.""" + if not self._param_def_block: + return False + return target in self._param_def_block[-1] + + def push_assign_tracking(self) -> None: + """Pushes a new layer for assignment tracking.""" + self._assign_stack.append(set()) + + def pop_assign_tracking(self, frame: Frame) -> None: + """Pops the topmost level for assignment tracking and updates the + context variables if necessary. + """ + vars = self._assign_stack.pop() + if ( + not frame.block_frame + and not frame.loop_frame + and not frame.toplevel + or not vars + ): + return + public_names = [x for x in vars if x[:1] != "_"] + if len(vars) == 1: + name = next(iter(vars)) + ref = frame.symbols.ref(name) + if frame.loop_frame: + self.writeline(f"_loop_vars[{name!r}] = {ref}") + return + if frame.block_frame: + self.writeline(f"_block_vars[{name!r}] = {ref}") + return + self.writeline(f"context.vars[{name!r}] = {ref}") + else: + if frame.loop_frame: + self.writeline("_loop_vars.update({") + elif frame.block_frame: + self.writeline("_block_vars.update({") + else: + self.writeline("context.vars.update({") + for idx, name in enumerate(sorted(vars)): + if idx: + self.write(", ") + ref = frame.symbols.ref(name) + self.write(f"{name!r}: {ref}") + self.write("})") + if not frame.block_frame and not frame.loop_frame and public_names: + if len(public_names) == 1: + self.writeline(f"context.exported_vars.add({public_names[0]!r})") + else: + names_str = ", ".join(map(repr, sorted(public_names))) + self.writeline(f"context.exported_vars.update(({names_str}))") + + # -- Statement Visitors + + def visit_Template( + self, node: nodes.Template, frame: t.Optional[Frame] = None + ) -> None: + assert frame is None, "no root frame allowed" + eval_ctx = EvalContext(self.environment, self.name) + + from .runtime import async_exported + from .runtime import exported + + if self.environment.is_async: + exported_names = sorted(exported + async_exported) + else: + exported_names = sorted(exported) + + self.writeline("from jinja2.runtime import " + ", ".join(exported_names)) + + # if we want a deferred initialization we cannot move the + # environment into a local name + envenv = "" if self.defer_init else ", environment=environment" + + # do we have an extends tag at all? If not, we can save some + # overhead by just not processing any inheritance code. + have_extends = node.find(nodes.Extends) is not None + + # find all blocks + for block in node.find_all(nodes.Block): + if block.name in self.blocks: + self.fail(f"block {block.name!r} defined twice", block.lineno) + self.blocks[block.name] = block + + # find all imports and import them + for import_ in node.find_all(nodes.ImportedName): + if import_.importname not in self.import_aliases: + imp = import_.importname + self.import_aliases[imp] = alias = self.temporary_identifier() + if "." in imp: + module, obj = imp.rsplit(".", 1) + self.writeline(f"from {module} import {obj} as {alias}") + else: + self.writeline(f"import {imp} as {alias}") + + # add the load name + self.writeline(f"name = {self.name!r}") + + # generate the root render function. + self.writeline( + f"{self.func('root')}(context, missing=missing{envenv}):", extra=1 + ) + self.indent() + self.write_commons() + + # process the root + frame = Frame(eval_ctx) + if "self" in find_undeclared(node.body, ("self",)): + ref = frame.symbols.declare_parameter("self") + self.writeline(f"{ref} = TemplateReference(context)") + frame.symbols.analyze_node(node) + frame.toplevel = frame.rootlevel = True + frame.require_output_check = have_extends and not self.has_known_extends + if have_extends: + self.writeline("parent_template = None") + self.enter_frame(frame) + self.pull_dependencies(node.body) + self.blockvisit(node.body, frame) + self.leave_frame(frame, with_python_scope=True) + self.outdent() + + # make sure that the parent root is called. + if have_extends: + if not self.has_known_extends: + self.indent() + self.writeline("if parent_template is not None:") + self.indent() + if not self.environment.is_async: + self.writeline("yield from parent_template.root_render_func(context)") + else: + self.writeline("agen = parent_template.root_render_func(context)") + self.writeline("try:") + self.indent() + self.writeline("async for event in agen:") + self.indent() + self.writeline("yield event") + self.outdent() + self.outdent() + self.writeline("finally: await agen.aclose()") + self.outdent(1 + (not self.has_known_extends)) + + # at this point we now have the blocks collected and can visit them too. + for name, block in self.blocks.items(): + self.writeline( + f"{self.func('block_' + name)}(context, missing=missing{envenv}):", + block, + 1, + ) + self.indent() + self.write_commons() + # It's important that we do not make this frame a child of the + # toplevel template. This would cause a variety of + # interesting issues with identifier tracking. + block_frame = Frame(eval_ctx) + block_frame.block_frame = True + undeclared = find_undeclared(block.body, ("self", "super")) + if "self" in undeclared: + ref = block_frame.symbols.declare_parameter("self") + self.writeline(f"{ref} = TemplateReference(context)") + if "super" in undeclared: + ref = block_frame.symbols.declare_parameter("super") + self.writeline(f"{ref} = context.super({name!r}, block_{name})") + block_frame.symbols.analyze_node(block) + block_frame.block = name + self.writeline("_block_vars = {}") + self.enter_frame(block_frame) + self.pull_dependencies(block.body) + self.blockvisit(block.body, block_frame) + self.leave_frame(block_frame, with_python_scope=True) + self.outdent() + + blocks_kv_str = ", ".join(f"{x!r}: block_{x}" for x in self.blocks) + self.writeline(f"blocks = {{{blocks_kv_str}}}", extra=1) + debug_kv_str = "&".join(f"{k}={v}" for k, v in self.debug_info) + self.writeline(f"debug_info = {debug_kv_str!r}") + + def visit_Block(self, node: nodes.Block, frame: Frame) -> None: + """Call a block and register it for the template.""" + level = 0 + if frame.toplevel: + # if we know that we are a child template, there is no need to + # check if we are one + if self.has_known_extends: + return + if self.extends_so_far > 0: + self.writeline("if parent_template is None:") + self.indent() + level += 1 + + if node.scoped: + context = self.derive_context(frame) + else: + context = self.get_context_ref() + + if node.required: + self.writeline(f"if len(context.blocks[{node.name!r}]) <= 1:", node) + self.indent() + self.writeline( + f'raise TemplateRuntimeError("Required block {node.name!r} not found")', + node, + ) + self.outdent() + + if not self.environment.is_async and frame.buffer is None: + self.writeline( + f"yield from context.blocks[{node.name!r}][0]({context})", node + ) + else: + self.writeline(f"gen = context.blocks[{node.name!r}][0]({context})") + self.writeline("try:") + self.indent() + self.writeline( + f"{self.choose_async()}for event in gen:", + node, + ) + self.indent() + self.simple_write("event", frame) + self.outdent() + self.outdent() + self.writeline( + f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}" + ) + + self.outdent(level) + + def visit_Extends(self, node: nodes.Extends, frame: Frame) -> None: + """Calls the extender.""" + if not frame.toplevel: + self.fail("cannot use extend from a non top-level scope", node.lineno) + + # if the number of extends statements in general is zero so + # far, we don't have to add a check if something extended + # the template before this one. + if self.extends_so_far > 0: + # if we have a known extends we just add a template runtime + # error into the generated code. We could catch that at compile + # time too, but i welcome it not to confuse users by throwing the + # same error at different times just "because we can". + if not self.has_known_extends: + self.writeline("if parent_template is not None:") + self.indent() + self.writeline('raise TemplateRuntimeError("extended multiple times")') + + # if we have a known extends already we don't need that code here + # as we know that the template execution will end here. + if self.has_known_extends: + raise CompilerExit() + else: + self.outdent() + + self.writeline("parent_template = environment.get_template(", node) + self.visit(node.template, frame) + self.write(f", {self.name!r})") + self.writeline("for name, parent_block in parent_template.blocks.items():") + self.indent() + self.writeline("context.blocks.setdefault(name, []).append(parent_block)") + self.outdent() + + # if this extends statement was in the root level we can take + # advantage of that information and simplify the generated code + # in the top level from this point onwards + if frame.rootlevel: + self.has_known_extends = True + + # and now we have one more + self.extends_so_far += 1 + + def visit_Include(self, node: nodes.Include, frame: Frame) -> None: + """Handles includes.""" + if node.ignore_missing: + self.writeline("try:") + self.indent() + + func_name = "get_or_select_template" + if isinstance(node.template, nodes.Const): + if isinstance(node.template.value, str): + func_name = "get_template" + elif isinstance(node.template.value, (tuple, list)): + func_name = "select_template" + elif isinstance(node.template, (nodes.Tuple, nodes.List)): + func_name = "select_template" + + self.writeline(f"template = environment.{func_name}(", node) + self.visit(node.template, frame) + self.write(f", {self.name!r})") + if node.ignore_missing: + self.outdent() + self.writeline("except TemplateNotFound:") + self.indent() + self.writeline("pass") + self.outdent() + self.writeline("else:") + self.indent() + + def loop_body() -> None: + self.indent() + self.simple_write("event", frame) + self.outdent() + + if node.with_context: + self.writeline( + f"gen = template.root_render_func(" + "template.new_context(context.get_all(), True," + f" {self.dump_local_context(frame)}))" + ) + self.writeline("try:") + self.indent() + self.writeline(f"{self.choose_async()}for event in gen:") + loop_body() + self.outdent() + self.writeline( + f"finally: {self.choose_async('await gen.aclose()', 'gen.close()')}" + ) + elif self.environment.is_async: + self.writeline( + "for event in (await template._get_default_module_async())" + "._body_stream:" + ) + loop_body() + else: + self.writeline("yield from template._get_default_module()._body_stream") + + if node.ignore_missing: + self.outdent() + + def _import_common( + self, node: t.Union[nodes.Import, nodes.FromImport], frame: Frame + ) -> None: + self.write(f"{self.choose_async('await ')}environment.get_template(") + self.visit(node.template, frame) + self.write(f", {self.name!r}).") + + if node.with_context: + f_name = f"make_module{self.choose_async('_async')}" + self.write( + f"{f_name}(context.get_all(), True, {self.dump_local_context(frame)})" + ) + else: + self.write(f"_get_default_module{self.choose_async('_async')}(context)") + + def visit_Import(self, node: nodes.Import, frame: Frame) -> None: + """Visit regular imports.""" + self.writeline(f"{frame.symbols.ref(node.target)} = ", node) + if frame.toplevel: + self.write(f"context.vars[{node.target!r}] = ") + + self._import_common(node, frame) + + if frame.toplevel and not node.target.startswith("_"): + self.writeline(f"context.exported_vars.discard({node.target!r})") + + def visit_FromImport(self, node: nodes.FromImport, frame: Frame) -> None: + """Visit named imports.""" + self.newline(node) + self.write("included_template = ") + self._import_common(node, frame) + var_names = [] + discarded_names = [] + for name in node.names: + if isinstance(name, tuple): + name, alias = name + else: + alias = name + self.writeline( + f"{frame.symbols.ref(alias)} =" + f" getattr(included_template, {name!r}, missing)" + ) + self.writeline(f"if {frame.symbols.ref(alias)} is missing:") + self.indent() + # The position will contain the template name, and will be formatted + # into a string that will be compiled into an f-string. Curly braces + # in the name must be replaced with escapes so that they will not be + # executed as part of the f-string. + position = self.position(node).replace("{", "{{").replace("}", "}}") + message = ( + "the template {included_template.__name__!r}" + f" (imported on {position})" + f" does not export the requested name {name!r}" + ) + self.writeline( + f"{frame.symbols.ref(alias)} = undefined(f{message!r}, name={name!r})" + ) + self.outdent() + if frame.toplevel: + var_names.append(alias) + if not alias.startswith("_"): + discarded_names.append(alias) + + if var_names: + if len(var_names) == 1: + name = var_names[0] + self.writeline(f"context.vars[{name!r}] = {frame.symbols.ref(name)}") + else: + names_kv = ", ".join( + f"{name!r}: {frame.symbols.ref(name)}" for name in var_names + ) + self.writeline(f"context.vars.update({{{names_kv}}})") + if discarded_names: + if len(discarded_names) == 1: + self.writeline(f"context.exported_vars.discard({discarded_names[0]!r})") + else: + names_str = ", ".join(map(repr, discarded_names)) + self.writeline( + f"context.exported_vars.difference_update(({names_str}))" + ) + + def visit_For(self, node: nodes.For, frame: Frame) -> None: + loop_frame = frame.inner() + loop_frame.loop_frame = True + test_frame = frame.inner() + else_frame = frame.inner() + + # try to figure out if we have an extended loop. An extended loop + # is necessary if the loop is in recursive mode if the special loop + # variable is accessed in the body if the body is a scoped block. + extended_loop = ( + node.recursive + or "loop" + in find_undeclared(node.iter_child_nodes(only=("body",)), ("loop",)) + or any(block.scoped for block in node.find_all(nodes.Block)) + ) + + loop_ref = None + if extended_loop: + loop_ref = loop_frame.symbols.declare_parameter("loop") + + loop_frame.symbols.analyze_node(node, for_branch="body") + if node.else_: + else_frame.symbols.analyze_node(node, for_branch="else") + + if node.test: + loop_filter_func = self.temporary_identifier() + test_frame.symbols.analyze_node(node, for_branch="test") + self.writeline(f"{self.func(loop_filter_func)}(fiter):", node.test) + self.indent() + self.enter_frame(test_frame) + self.writeline(self.choose_async("async for ", "for ")) + self.visit(node.target, loop_frame) + self.write(" in ") + self.write(self.choose_async("auto_aiter(fiter)", "fiter")) + self.write(":") + self.indent() + self.writeline("if ", node.test) + self.visit(node.test, test_frame) + self.write(":") + self.indent() + self.writeline("yield ") + self.visit(node.target, loop_frame) + self.outdent(3) + self.leave_frame(test_frame, with_python_scope=True) + + # if we don't have an recursive loop we have to find the shadowed + # variables at that point. Because loops can be nested but the loop + # variable is a special one we have to enforce aliasing for it. + if node.recursive: + self.writeline( + f"{self.func('loop')}(reciter, loop_render_func, depth=0):", node + ) + self.indent() + self.buffer(loop_frame) + + # Use the same buffer for the else frame + else_frame.buffer = loop_frame.buffer + + # make sure the loop variable is a special one and raise a template + # assertion error if a loop tries to write to loop + if extended_loop: + self.writeline(f"{loop_ref} = missing") + + for name in node.find_all(nodes.Name): + if name.ctx == "store" and name.name == "loop": + self.fail( + "Can't assign to special loop variable in for-loop target", + name.lineno, + ) + + if node.else_: + iteration_indicator = self.temporary_identifier() + self.writeline(f"{iteration_indicator} = 1") + + self.writeline(self.choose_async("async for ", "for "), node) + self.visit(node.target, loop_frame) + if extended_loop: + self.write(f", {loop_ref} in {self.choose_async('Async')}LoopContext(") + else: + self.write(" in ") + + if node.test: + self.write(f"{loop_filter_func}(") + if node.recursive: + self.write("reciter") + else: + if self.environment.is_async and not extended_loop: + self.write("auto_aiter(") + self.visit(node.iter, frame) + if self.environment.is_async and not extended_loop: + self.write(")") + if node.test: + self.write(")") + + if node.recursive: + self.write(", undefined, loop_render_func, depth):") + else: + self.write(", undefined):" if extended_loop else ":") + + self.indent() + self.enter_frame(loop_frame) + + self.writeline("_loop_vars = {}") + self.blockvisit(node.body, loop_frame) + if node.else_: + self.writeline(f"{iteration_indicator} = 0") + self.outdent() + self.leave_frame( + loop_frame, with_python_scope=node.recursive and not node.else_ + ) + + if node.else_: + self.writeline(f"if {iteration_indicator}:") + self.indent() + self.enter_frame(else_frame) + self.blockvisit(node.else_, else_frame) + self.leave_frame(else_frame) + self.outdent() + + # if the node was recursive we have to return the buffer contents + # and start the iteration code + if node.recursive: + self.return_buffer_contents(loop_frame) + self.outdent() + self.start_write(frame, node) + self.write(f"{self.choose_async('await ')}loop(") + if self.environment.is_async: + self.write("auto_aiter(") + self.visit(node.iter, frame) + if self.environment.is_async: + self.write(")") + self.write(", loop)") + self.end_write(frame) + + # at the end of the iteration, clear any assignments made in the + # loop from the top level + if self._assign_stack: + self._assign_stack[-1].difference_update(loop_frame.symbols.stores) + + def visit_If(self, node: nodes.If, frame: Frame) -> None: + if_frame = frame.soft() + self.writeline("if ", node) + self.visit(node.test, if_frame) + self.write(":") + self.indent() + self.blockvisit(node.body, if_frame) + self.outdent() + for elif_ in node.elif_: + self.writeline("elif ", elif_) + self.visit(elif_.test, if_frame) + self.write(":") + self.indent() + self.blockvisit(elif_.body, if_frame) + self.outdent() + if node.else_: + self.writeline("else:") + self.indent() + self.blockvisit(node.else_, if_frame) + self.outdent() + + def visit_Macro(self, node: nodes.Macro, frame: Frame) -> None: + macro_frame, macro_ref = self.macro_body(node, frame) + self.newline() + if frame.toplevel: + if not node.name.startswith("_"): + self.write(f"context.exported_vars.add({node.name!r})") + self.writeline(f"context.vars[{node.name!r}] = ") + self.write(f"{frame.symbols.ref(node.name)} = ") + self.macro_def(macro_ref, macro_frame) + + def visit_CallBlock(self, node: nodes.CallBlock, frame: Frame) -> None: + call_frame, macro_ref = self.macro_body(node, frame) + self.writeline("caller = ") + self.macro_def(macro_ref, call_frame) + self.start_write(frame, node) + self.visit_Call(node.call, frame, forward_caller=True) + self.end_write(frame) + + def visit_FilterBlock(self, node: nodes.FilterBlock, frame: Frame) -> None: + filter_frame = frame.inner() + filter_frame.symbols.analyze_node(node) + self.enter_frame(filter_frame) + self.buffer(filter_frame) + self.blockvisit(node.body, filter_frame) + self.start_write(frame, node) + self.visit_Filter(node.filter, filter_frame) + self.end_write(frame) + self.leave_frame(filter_frame) + + def visit_With(self, node: nodes.With, frame: Frame) -> None: + with_frame = frame.inner() + with_frame.symbols.analyze_node(node) + self.enter_frame(with_frame) + for target, expr in zip(node.targets, node.values): + self.newline() + self.visit(target, with_frame) + self.write(" = ") + self.visit(expr, frame) + self.blockvisit(node.body, with_frame) + self.leave_frame(with_frame) + + def visit_ExprStmt(self, node: nodes.ExprStmt, frame: Frame) -> None: + self.newline(node) + self.visit(node.node, frame) + + class _FinalizeInfo(t.NamedTuple): + const: t.Optional[t.Callable[..., str]] + src: t.Optional[str] + + @staticmethod + def _default_finalize(value: t.Any) -> t.Any: + """The default finalize function if the environment isn't + configured with one. Or, if the environment has one, this is + called on that function's output for constants. + """ + return str(value) + + _finalize: t.Optional[_FinalizeInfo] = None + + def _make_finalize(self) -> _FinalizeInfo: + """Build the finalize function to be used on constants and at + runtime. Cached so it's only created once for all output nodes. + + Returns a ``namedtuple`` with the following attributes: + + ``const`` + A function to finalize constant data at compile time. + + ``src`` + Source code to output around nodes to be evaluated at + runtime. + """ + if self._finalize is not None: + return self._finalize + + finalize: t.Optional[t.Callable[..., t.Any]] + finalize = default = self._default_finalize + src = None + + if self.environment.finalize: + src = "environment.finalize(" + env_finalize = self.environment.finalize + pass_arg = { + _PassArg.context: "context", + _PassArg.eval_context: "context.eval_ctx", + _PassArg.environment: "environment", + }.get( + _PassArg.from_obj(env_finalize) # type: ignore + ) + finalize = None + + if pass_arg is None: + + def finalize(value: t.Any) -> t.Any: # noqa: F811 + return default(env_finalize(value)) + + else: + src = f"{src}{pass_arg}, " + + if pass_arg == "environment": + + def finalize(value: t.Any) -> t.Any: # noqa: F811 + return default(env_finalize(self.environment, value)) + + self._finalize = self._FinalizeInfo(finalize, src) + return self._finalize + + def _output_const_repr(self, group: t.Iterable[t.Any]) -> str: + """Given a group of constant values converted from ``Output`` + child nodes, produce a string to write to the template module + source. + """ + return repr(concat(group)) + + def _output_child_to_const( + self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo + ) -> str: + """Try to optimize a child of an ``Output`` node by trying to + convert it to constant, finalized data at compile time. + + If :exc:`Impossible` is raised, the node is not constant and + will be evaluated at runtime. Any other exception will also be + evaluated at runtime for easier debugging. + """ + const = node.as_const(frame.eval_ctx) + + if frame.eval_ctx.autoescape: + const = escape(const) + + # Template data doesn't go through finalize. + if isinstance(node, nodes.TemplateData): + return str(const) + + return finalize.const(const) # type: ignore + + def _output_child_pre( + self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo + ) -> None: + """Output extra source code before visiting a child of an + ``Output`` node. + """ + if frame.eval_ctx.volatile: + self.write("(escape if context.eval_ctx.autoescape else str)(") + elif frame.eval_ctx.autoescape: + self.write("escape(") + else: + self.write("str(") + + if finalize.src is not None: + self.write(finalize.src) + + def _output_child_post( + self, node: nodes.Expr, frame: Frame, finalize: _FinalizeInfo + ) -> None: + """Output extra source code after visiting a child of an + ``Output`` node. + """ + self.write(")") + + if finalize.src is not None: + self.write(")") + + def visit_Output(self, node: nodes.Output, frame: Frame) -> None: + # If an extends is active, don't render outside a block. + if frame.require_output_check: + # A top-level extends is known to exist at compile time. + if self.has_known_extends: + return + + self.writeline("if parent_template is None:") + self.indent() + + finalize = self._make_finalize() + body: t.List[t.Union[t.List[t.Any], nodes.Expr]] = [] + + # Evaluate constants at compile time if possible. Each item in + # body will be either a list of static data or a node to be + # evaluated at runtime. + for child in node.nodes: + try: + if not ( + # If the finalize function requires runtime context, + # constants can't be evaluated at compile time. + finalize.const + # Unless it's basic template data that won't be + # finalized anyway. + or isinstance(child, nodes.TemplateData) + ): + raise nodes.Impossible() + + const = self._output_child_to_const(child, frame, finalize) + except (nodes.Impossible, Exception): + # The node was not constant and needs to be evaluated at + # runtime. Or another error was raised, which is easier + # to debug at runtime. + body.append(child) + continue + + if body and isinstance(body[-1], list): + body[-1].append(const) + else: + body.append([const]) + + if frame.buffer is not None: + if len(body) == 1: + self.writeline(f"{frame.buffer}.append(") + else: + self.writeline(f"{frame.buffer}.extend((") + + self.indent() + + for item in body: + if isinstance(item, list): + # A group of constant data to join and output. + val = self._output_const_repr(item) + + if frame.buffer is None: + self.writeline("yield " + val) + else: + self.writeline(val + ",") + else: + if frame.buffer is None: + self.writeline("yield ", item) + else: + self.newline(item) + + # A node to be evaluated at runtime. + self._output_child_pre(item, frame, finalize) + self.visit(item, frame) + self._output_child_post(item, frame, finalize) + + if frame.buffer is not None: + self.write(",") + + if frame.buffer is not None: + self.outdent() + self.writeline(")" if len(body) == 1 else "))") + + if frame.require_output_check: + self.outdent() + + def visit_Assign(self, node: nodes.Assign, frame: Frame) -> None: + self.push_assign_tracking() + + # ``a.b`` is allowed for assignment, and is parsed as an NSRef. However, + # it is only valid if it references a Namespace object. Emit a check for + # that for each ref here, before assignment code is emitted. This can't + # be done in visit_NSRef as the ref could be in the middle of a tuple. + seen_refs: t.Set[str] = set() + + for nsref in node.find_all(nodes.NSRef): + if nsref.name in seen_refs: + # Only emit the check for each reference once, in case the same + # ref is used multiple times in a tuple, `ns.a, ns.b = c, d`. + continue + + seen_refs.add(nsref.name) + ref = frame.symbols.ref(nsref.name) + self.writeline(f"if not isinstance({ref}, Namespace):") + self.indent() + self.writeline( + "raise TemplateRuntimeError" + '("cannot assign attribute on non-namespace object")' + ) + self.outdent() + + self.newline(node) + self.visit(node.target, frame) + self.write(" = ") + self.visit(node.node, frame) + self.pop_assign_tracking(frame) + + def visit_AssignBlock(self, node: nodes.AssignBlock, frame: Frame) -> None: + self.push_assign_tracking() + block_frame = frame.inner() + # This is a special case. Since a set block always captures we + # will disable output checks. This way one can use set blocks + # toplevel even in extended templates. + block_frame.require_output_check = False + block_frame.symbols.analyze_node(node) + self.enter_frame(block_frame) + self.buffer(block_frame) + self.blockvisit(node.body, block_frame) + self.newline(node) + self.visit(node.target, frame) + self.write(" = (Markup if context.eval_ctx.autoescape else identity)(") + if node.filter is not None: + self.visit_Filter(node.filter, block_frame) + else: + self.write(f"concat({block_frame.buffer})") + self.write(")") + self.pop_assign_tracking(frame) + self.leave_frame(block_frame) + + # -- Expression Visitors + + def visit_Name(self, node: nodes.Name, frame: Frame) -> None: + if node.ctx == "store" and ( + frame.toplevel or frame.loop_frame or frame.block_frame + ): + if self._assign_stack: + self._assign_stack[-1].add(node.name) + ref = frame.symbols.ref(node.name) + + # If we are looking up a variable we might have to deal with the + # case where it's undefined. We can skip that case if the load + # instruction indicates a parameter which are always defined. + if node.ctx == "load": + load = frame.symbols.find_load(ref) + if not ( + load is not None + and load[0] == VAR_LOAD_PARAMETER + and not self.parameter_is_undeclared(ref) + ): + self.write( + f"(undefined(name={node.name!r}) if {ref} is missing else {ref})" + ) + return + + self.write(ref) + + def visit_NSRef(self, node: nodes.NSRef, frame: Frame) -> None: + # NSRef is a dotted assignment target a.b=c, but uses a[b]=c internally. + # visit_Assign emits code to validate that each ref is to a Namespace + # object only. That can't be emitted here as the ref could be in the + # middle of a tuple assignment. + ref = frame.symbols.ref(node.name) + self.writeline(f"{ref}[{node.attr!r}]") + + def visit_Const(self, node: nodes.Const, frame: Frame) -> None: + val = node.as_const(frame.eval_ctx) + if isinstance(val, float): + self.write(str(val)) + else: + self.write(repr(val)) + + def visit_TemplateData(self, node: nodes.TemplateData, frame: Frame) -> None: + try: + self.write(repr(node.as_const(frame.eval_ctx))) + except nodes.Impossible: + self.write( + f"(Markup if context.eval_ctx.autoescape else identity)({node.data!r})" + ) + + def visit_Tuple(self, node: nodes.Tuple, frame: Frame) -> None: + self.write("(") + idx = -1 + for idx, item in enumerate(node.items): + if idx: + self.write(", ") + self.visit(item, frame) + self.write(",)" if idx == 0 else ")") + + def visit_List(self, node: nodes.List, frame: Frame) -> None: + self.write("[") + for idx, item in enumerate(node.items): + if idx: + self.write(", ") + self.visit(item, frame) + self.write("]") + + def visit_Dict(self, node: nodes.Dict, frame: Frame) -> None: + self.write("{") + for idx, item in enumerate(node.items): + if idx: + self.write(", ") + self.visit(item.key, frame) + self.write(": ") + self.visit(item.value, frame) + self.write("}") + + visit_Add = _make_binop("+") + visit_Sub = _make_binop("-") + visit_Mul = _make_binop("*") + visit_Div = _make_binop("/") + visit_FloorDiv = _make_binop("//") + visit_Pow = _make_binop("**") + visit_Mod = _make_binop("%") + visit_And = _make_binop("and") + visit_Or = _make_binop("or") + visit_Pos = _make_unop("+") + visit_Neg = _make_unop("-") + visit_Not = _make_unop("not ") + + @optimizeconst + def visit_Concat(self, node: nodes.Concat, frame: Frame) -> None: + if frame.eval_ctx.volatile: + func_name = "(markup_join if context.eval_ctx.volatile else str_join)" + elif frame.eval_ctx.autoescape: + func_name = "markup_join" + else: + func_name = "str_join" + self.write(f"{func_name}((") + for arg in node.nodes: + self.visit(arg, frame) + self.write(", ") + self.write("))") + + @optimizeconst + def visit_Compare(self, node: nodes.Compare, frame: Frame) -> None: + self.write("(") + self.visit(node.expr, frame) + for op in node.ops: + self.visit(op, frame) + self.write(")") + + def visit_Operand(self, node: nodes.Operand, frame: Frame) -> None: + self.write(f" {operators[node.op]} ") + self.visit(node.expr, frame) + + @optimizeconst + def visit_Getattr(self, node: nodes.Getattr, frame: Frame) -> None: + if self.environment.is_async: + self.write("(await auto_await(") + + self.write("environment.getattr(") + self.visit(node.node, frame) + self.write(f", {node.attr!r})") + + if self.environment.is_async: + self.write("))") + + @optimizeconst + def visit_Getitem(self, node: nodes.Getitem, frame: Frame) -> None: + # slices bypass the environment getitem method. + if isinstance(node.arg, nodes.Slice): + self.visit(node.node, frame) + self.write("[") + self.visit(node.arg, frame) + self.write("]") + else: + if self.environment.is_async: + self.write("(await auto_await(") + + self.write("environment.getitem(") + self.visit(node.node, frame) + self.write(", ") + self.visit(node.arg, frame) + self.write(")") + + if self.environment.is_async: + self.write("))") + + def visit_Slice(self, node: nodes.Slice, frame: Frame) -> None: + if node.start is not None: + self.visit(node.start, frame) + self.write(":") + if node.stop is not None: + self.visit(node.stop, frame) + if node.step is not None: + self.write(":") + self.visit(node.step, frame) + + @contextmanager + def _filter_test_common( + self, node: t.Union[nodes.Filter, nodes.Test], frame: Frame, is_filter: bool + ) -> t.Iterator[None]: + if self.environment.is_async: + self.write("(await auto_await(") + + if is_filter: + self.write(f"{self.filters[node.name]}(") + func = self.environment.filters.get(node.name) + else: + self.write(f"{self.tests[node.name]}(") + func = self.environment.tests.get(node.name) + + # When inside an If or CondExpr frame, allow the filter to be + # undefined at compile time and only raise an error if it's + # actually called at runtime. See pull_dependencies. + if func is None and not frame.soft_frame: + type_name = "filter" if is_filter else "test" + self.fail(f"No {type_name} named {node.name!r}.", node.lineno) + + pass_arg = { + _PassArg.context: "context", + _PassArg.eval_context: "context.eval_ctx", + _PassArg.environment: "environment", + }.get( + _PassArg.from_obj(func) # type: ignore + ) + + if pass_arg is not None: + self.write(f"{pass_arg}, ") + + # Back to the visitor function to handle visiting the target of + # the filter or test. + yield + + self.signature(node, frame) + self.write(")") + + if self.environment.is_async: + self.write("))") + + @optimizeconst + def visit_Filter(self, node: nodes.Filter, frame: Frame) -> None: + with self._filter_test_common(node, frame, True): + # if the filter node is None we are inside a filter block + # and want to write to the current buffer + if node.node is not None: + self.visit(node.node, frame) + elif frame.eval_ctx.volatile: + self.write( + f"(Markup(concat({frame.buffer}))" + f" if context.eval_ctx.autoescape else concat({frame.buffer}))" + ) + elif frame.eval_ctx.autoescape: + self.write(f"Markup(concat({frame.buffer}))") + else: + self.write(f"concat({frame.buffer})") + + @optimizeconst + def visit_Test(self, node: nodes.Test, frame: Frame) -> None: + with self._filter_test_common(node, frame, False): + self.visit(node.node, frame) + + @optimizeconst + def visit_CondExpr(self, node: nodes.CondExpr, frame: Frame) -> None: + frame = frame.soft() + + def write_expr2() -> None: + if node.expr2 is not None: + self.visit(node.expr2, frame) + return + + self.write( + f'cond_expr_undefined("the inline if-expression on' + f" {self.position(node)} evaluated to false and no else" + f' section was defined.")' + ) + + self.write("(") + self.visit(node.expr1, frame) + self.write(" if ") + self.visit(node.test, frame) + self.write(" else ") + write_expr2() + self.write(")") + + @optimizeconst + def visit_Call( + self, node: nodes.Call, frame: Frame, forward_caller: bool = False + ) -> None: + if self.environment.is_async: + self.write("(await auto_await(") + if self.environment.sandboxed: + self.write("environment.call(context, ") + else: + self.write("context.call(") + self.visit(node.node, frame) + extra_kwargs = {"caller": "caller"} if forward_caller else None + loop_kwargs = {"_loop_vars": "_loop_vars"} if frame.loop_frame else {} + block_kwargs = {"_block_vars": "_block_vars"} if frame.block_frame else {} + if extra_kwargs: + extra_kwargs.update(loop_kwargs, **block_kwargs) + elif loop_kwargs or block_kwargs: + extra_kwargs = dict(loop_kwargs, **block_kwargs) + self.signature(node, frame, extra_kwargs) + self.write(")") + if self.environment.is_async: + self.write("))") + + def visit_Keyword(self, node: nodes.Keyword, frame: Frame) -> None: + self.write(node.key + "=") + self.visit(node.value, frame) + + # -- Unused nodes for extensions + + def visit_MarkSafe(self, node: nodes.MarkSafe, frame: Frame) -> None: + self.write("Markup(") + self.visit(node.expr, frame) + self.write(")") + + def visit_MarkSafeIfAutoescape( + self, node: nodes.MarkSafeIfAutoescape, frame: Frame + ) -> None: + self.write("(Markup if context.eval_ctx.autoescape else identity)(") + self.visit(node.expr, frame) + self.write(")") + + def visit_EnvironmentAttribute( + self, node: nodes.EnvironmentAttribute, frame: Frame + ) -> None: + self.write("environment." + node.name) + + def visit_ExtensionAttribute( + self, node: nodes.ExtensionAttribute, frame: Frame + ) -> None: + self.write(f"environment.extensions[{node.identifier!r}].{node.name}") + + def visit_ImportedName(self, node: nodes.ImportedName, frame: Frame) -> None: + self.write(self.import_aliases[node.importname]) + + def visit_InternalName(self, node: nodes.InternalName, frame: Frame) -> None: + self.write(node.name) + + def visit_ContextReference( + self, node: nodes.ContextReference, frame: Frame + ) -> None: + self.write("context") + + def visit_DerivedContextReference( + self, node: nodes.DerivedContextReference, frame: Frame + ) -> None: + self.write(self.derive_context(frame)) + + def visit_Continue(self, node: nodes.Continue, frame: Frame) -> None: + self.writeline("continue", node) + + def visit_Break(self, node: nodes.Break, frame: Frame) -> None: + self.writeline("break", node) + + def visit_Scope(self, node: nodes.Scope, frame: Frame) -> None: + scope_frame = frame.inner() + scope_frame.symbols.analyze_node(node) + self.enter_frame(scope_frame) + self.blockvisit(node.body, scope_frame) + self.leave_frame(scope_frame) + + def visit_OverlayScope(self, node: nodes.OverlayScope, frame: Frame) -> None: + ctx = self.temporary_identifier() + self.writeline(f"{ctx} = {self.derive_context(frame)}") + self.writeline(f"{ctx}.vars = ") + self.visit(node.context, frame) + self.push_context_reference(ctx) + + scope_frame = frame.inner(isolated=True) + scope_frame.symbols.analyze_node(node) + self.enter_frame(scope_frame) + self.blockvisit(node.body, scope_frame) + self.leave_frame(scope_frame) + self.pop_context_reference() + + def visit_EvalContextModifier( + self, node: nodes.EvalContextModifier, frame: Frame + ) -> None: + for keyword in node.options: + self.writeline(f"context.eval_ctx.{keyword.key} = ") + self.visit(keyword.value, frame) + try: + val = keyword.value.as_const(frame.eval_ctx) + except nodes.Impossible: + frame.eval_ctx.volatile = True + else: + setattr(frame.eval_ctx, keyword.key, val) + + def visit_ScopedEvalContextModifier( + self, node: nodes.ScopedEvalContextModifier, frame: Frame + ) -> None: + old_ctx_name = self.temporary_identifier() + saved_ctx = frame.eval_ctx.save() + self.writeline(f"{old_ctx_name} = context.eval_ctx.save()") + self.visit_EvalContextModifier(node, frame) + for child in node.body: + self.visit(child, frame) + frame.eval_ctx.revert(saved_ctx) + self.writeline(f"context.eval_ctx.revert({old_ctx_name})") diff --git a/netdeploy/lib/python3.11/site-packages/jinja2/constants.py b/netdeploy/lib/python3.11/site-packages/jinja2/constants.py new file mode 100644 index 0000000..41a1c23 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2/constants.py @@ -0,0 +1,20 @@ +#: list of lorem ipsum words used by the lipsum() helper function +LOREM_IPSUM_WORDS = """\ +a ac accumsan ad adipiscing aenean aliquam aliquet amet ante aptent arcu at +auctor augue bibendum blandit class commodo condimentum congue consectetuer +consequat conubia convallis cras cubilia cum curabitur curae cursus dapibus +diam dictum dictumst dignissim dis dolor donec dui duis egestas eget eleifend +elementum elit enim erat eros est et etiam eu euismod facilisi facilisis fames +faucibus felis fermentum feugiat fringilla fusce gravida habitant habitasse hac +hendrerit hymenaeos iaculis id imperdiet in inceptos integer interdum ipsum +justo lacinia lacus laoreet lectus leo libero ligula litora lobortis lorem +luctus maecenas magna magnis malesuada massa mattis mauris metus mi molestie +mollis montes morbi mus nam nascetur natoque nec neque netus nibh nisi nisl non +nonummy nostra nulla nullam nunc odio orci ornare parturient pede pellentesque +penatibus per pharetra phasellus placerat platea porta porttitor posuere +potenti praesent pretium primis proin pulvinar purus quam quis quisque rhoncus +ridiculus risus rutrum sagittis sapien scelerisque sed sem semper senectus sit +sociis sociosqu sodales sollicitudin suscipit suspendisse taciti tellus tempor +tempus tincidunt torquent tortor tristique turpis ullamcorper ultrices +ultricies urna ut varius vehicula vel velit venenatis vestibulum vitae vivamus +viverra volutpat vulputate""" diff --git a/netdeploy/lib/python3.11/site-packages/jinja2/debug.py b/netdeploy/lib/python3.11/site-packages/jinja2/debug.py new file mode 100644 index 0000000..eeeeee7 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2/debug.py @@ -0,0 +1,191 @@ +import sys +import typing as t +from types import CodeType +from types import TracebackType + +from .exceptions import TemplateSyntaxError +from .utils import internal_code +from .utils import missing + +if t.TYPE_CHECKING: + from .runtime import Context + + +def rewrite_traceback_stack(source: t.Optional[str] = None) -> BaseException: + """Rewrite the current exception to replace any tracebacks from + within compiled template code with tracebacks that look like they + came from the template source. + + This must be called within an ``except`` block. + + :param source: For ``TemplateSyntaxError``, the original source if + known. + :return: The original exception with the rewritten traceback. + """ + _, exc_value, tb = sys.exc_info() + exc_value = t.cast(BaseException, exc_value) + tb = t.cast(TracebackType, tb) + + if isinstance(exc_value, TemplateSyntaxError) and not exc_value.translated: + exc_value.translated = True + exc_value.source = source + # Remove the old traceback, otherwise the frames from the + # compiler still show up. + exc_value.with_traceback(None) + # Outside of runtime, so the frame isn't executing template + # code, but it still needs to point at the template. + tb = fake_traceback( + exc_value, None, exc_value.filename or "", exc_value.lineno + ) + else: + # Skip the frame for the render function. + tb = tb.tb_next + + stack = [] + + # Build the stack of traceback object, replacing any in template + # code with the source file and line information. + while tb is not None: + # Skip frames decorated with @internalcode. These are internal + # calls that aren't useful in template debugging output. + if tb.tb_frame.f_code in internal_code: + tb = tb.tb_next + continue + + template = tb.tb_frame.f_globals.get("__jinja_template__") + + if template is not None: + lineno = template.get_corresponding_lineno(tb.tb_lineno) + fake_tb = fake_traceback(exc_value, tb, template.filename, lineno) + stack.append(fake_tb) + else: + stack.append(tb) + + tb = tb.tb_next + + tb_next = None + + # Assign tb_next in reverse to avoid circular references. + for tb in reversed(stack): + tb.tb_next = tb_next + tb_next = tb + + return exc_value.with_traceback(tb_next) + + +def fake_traceback( # type: ignore + exc_value: BaseException, tb: t.Optional[TracebackType], filename: str, lineno: int +) -> TracebackType: + """Produce a new traceback object that looks like it came from the + template source instead of the compiled code. The filename, line + number, and location name will point to the template, and the local + variables will be the current template context. + + :param exc_value: The original exception to be re-raised to create + the new traceback. + :param tb: The original traceback to get the local variables and + code info from. + :param filename: The template filename. + :param lineno: The line number in the template source. + """ + if tb is not None: + # Replace the real locals with the context that would be + # available at that point in the template. + locals = get_template_locals(tb.tb_frame.f_locals) + locals.pop("__jinja_exception__", None) + else: + locals = {} + + globals = { + "__name__": filename, + "__file__": filename, + "__jinja_exception__": exc_value, + } + # Raise an exception at the correct line number. + code: CodeType = compile( + "\n" * (lineno - 1) + "raise __jinja_exception__", filename, "exec" + ) + + # Build a new code object that points to the template file and + # replaces the location with a block name. + location = "template" + + if tb is not None: + function = tb.tb_frame.f_code.co_name + + if function == "root": + location = "top-level template code" + elif function.startswith("block_"): + location = f"block {function[6:]!r}" + + if sys.version_info >= (3, 8): + code = code.replace(co_name=location) + else: + code = CodeType( + code.co_argcount, + code.co_kwonlyargcount, + code.co_nlocals, + code.co_stacksize, + code.co_flags, + code.co_code, + code.co_consts, + code.co_names, + code.co_varnames, + code.co_filename, + location, + code.co_firstlineno, + code.co_lnotab, + code.co_freevars, + code.co_cellvars, + ) + + # Execute the new code, which is guaranteed to raise, and return + # the new traceback without this frame. + try: + exec(code, globals, locals) + except BaseException: + return sys.exc_info()[2].tb_next # type: ignore + + +def get_template_locals(real_locals: t.Mapping[str, t.Any]) -> t.Dict[str, t.Any]: + """Based on the runtime locals, get the context that would be + available at that point in the template. + """ + # Start with the current template context. + ctx: t.Optional[Context] = real_locals.get("context") + + if ctx is not None: + data: t.Dict[str, t.Any] = ctx.get_all().copy() + else: + data = {} + + # Might be in a derived context that only sets local variables + # rather than pushing a context. Local variables follow the scheme + # l_depth_name. Find the highest-depth local that has a value for + # each name. + local_overrides: t.Dict[str, t.Tuple[int, t.Any]] = {} + + for name, value in real_locals.items(): + if not name.startswith("l_") or value is missing: + # Not a template variable, or no longer relevant. + continue + + try: + _, depth_str, name = name.split("_", 2) + depth = int(depth_str) + except ValueError: + continue + + cur_depth = local_overrides.get(name, (-1,))[0] + + if cur_depth < depth: + local_overrides[name] = (depth, value) + + # Modify the context with any derived context. + for name, (_, value) in local_overrides.items(): + if value is missing: + data.pop(name, None) + else: + data[name] = value + + return data diff --git a/netdeploy/lib/python3.11/site-packages/jinja2/defaults.py b/netdeploy/lib/python3.11/site-packages/jinja2/defaults.py new file mode 100644 index 0000000..638cad3 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2/defaults.py @@ -0,0 +1,48 @@ +import typing as t + +from .filters import FILTERS as DEFAULT_FILTERS # noqa: F401 +from .tests import TESTS as DEFAULT_TESTS # noqa: F401 +from .utils import Cycler +from .utils import generate_lorem_ipsum +from .utils import Joiner +from .utils import Namespace + +if t.TYPE_CHECKING: + import typing_extensions as te + +# defaults for the parser / lexer +BLOCK_START_STRING = "{%" +BLOCK_END_STRING = "%}" +VARIABLE_START_STRING = "{{" +VARIABLE_END_STRING = "}}" +COMMENT_START_STRING = "{#" +COMMENT_END_STRING = "#}" +LINE_STATEMENT_PREFIX: t.Optional[str] = None +LINE_COMMENT_PREFIX: t.Optional[str] = None +TRIM_BLOCKS = False +LSTRIP_BLOCKS = False +NEWLINE_SEQUENCE: "te.Literal['\\n', '\\r\\n', '\\r']" = "\n" +KEEP_TRAILING_NEWLINE = False + +# default filters, tests and namespace + +DEFAULT_NAMESPACE = { + "range": range, + "dict": dict, + "lipsum": generate_lorem_ipsum, + "cycler": Cycler, + "joiner": Joiner, + "namespace": Namespace, +} + +# default policies +DEFAULT_POLICIES: t.Dict[str, t.Any] = { + "compiler.ascii_str": True, + "urlize.rel": "noopener", + "urlize.target": None, + "urlize.extra_schemes": None, + "truncate.leeway": 5, + "json.dumps_function": None, + "json.dumps_kwargs": {"sort_keys": True}, + "ext.i18n.trimmed": False, +} diff --git a/netdeploy/lib/python3.11/site-packages/jinja2/environment.py b/netdeploy/lib/python3.11/site-packages/jinja2/environment.py new file mode 100644 index 0000000..0fc6e5b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/jinja2/environment.py @@ -0,0 +1,1672 @@ +"""Classes for managing templates and their runtime and compile time +options. +""" + +import os +import typing +import typing as t +import weakref +from collections import ChainMap +from functools import lru_cache +from functools import partial +from functools import reduce +from types import CodeType + +from markupsafe import Markup + +from . import nodes +from .compiler import CodeGenerator +from .compiler import generate +from .defaults import BLOCK_END_STRING +from .defaults import BLOCK_START_STRING +from .defaults import COMMENT_END_STRING +from .defaults import COMMENT_START_STRING +from .defaults import DEFAULT_FILTERS # type: ignore[attr-defined] +from .defaults import DEFAULT_NAMESPACE +from .defaults import DEFAULT_POLICIES +from .defaults import DEFAULT_TESTS # type: ignore[attr-defined] +from .defaults import KEEP_TRAILING_NEWLINE +from .defaults import LINE_COMMENT_PREFIX +from .defaults import LINE_STATEMENT_PREFIX +from .defaults import LSTRIP_BLOCKS +from .defaults import NEWLINE_SEQUENCE +from .defaults import TRIM_BLOCKS +from .defaults import VARIABLE_END_STRING +from .defaults import VARIABLE_START_STRING +from .exceptions import TemplateNotFound +from .exceptions import TemplateRuntimeError +from .exceptions import TemplatesNotFound +from .exceptions import TemplateSyntaxError +from .exceptions import UndefinedError +from .lexer import get_lexer +from .lexer import Lexer +from .lexer import TokenStream +from .nodes import EvalContext +from .parser import Parser +from .runtime import Context +from .runtime import new_context +from .runtime import Undefined +from .utils import _PassArg +from .utils import concat +from .utils import consume +from .utils import import_string +from .utils import internalcode +from .utils import LRUCache +from .utils import missing + +if t.TYPE_CHECKING: + import typing_extensions as te + + from .bccache import BytecodeCache + from .ext import Extension + from .loaders import BaseLoader + +_env_bound = t.TypeVar("_env_bound", bound="Environment") + + +# for direct template usage we have up to ten living environments +@lru_cache(maxsize=10) +def get_spontaneous_environment(cls: t.Type[_env_bound], *args: t.Any) -> _env_bound: + """Return a new spontaneous environment. A spontaneous environment + is used for templates created directly rather than through an + existing environment. + + :param cls: Environment class to create. + :param args: Positional arguments passed to environment. + """ + env = cls(*args) + env.shared = True + return env + + +def create_cache( + size: int, +) -> t.Optional[t.MutableMapping[t.Tuple["weakref.ref[t.Any]", str], "Template"]]: + """Return the cache class for the given size.""" + if size == 0: + return None + + if size < 0: + return {} + + return LRUCache(size) # type: ignore + + +def copy_cache( + cache: t.Optional[t.MutableMapping[t.Any, t.Any]], +) -> t.Optional[t.MutableMapping[t.Tuple["weakref.ref[t.Any]", str], "Template"]]: + """Create an empty copy of the given cache.""" + if cache is None: + return None + + if type(cache) is dict: # noqa E721 + return {} + + return LRUCache(cache.capacity) # type: ignore + + +def load_extensions( + environment: "Environment", + extensions: t.Sequence[t.Union[str, t.Type["Extension"]]], +) -> t.Dict[str, "Extension"]: + """Load the extensions from the list and bind it to the environment. + Returns a dict of instantiated extensions. + """ + result = {} + + for extension in extensions: + if isinstance(extension, str): + extension = t.cast(t.Type["Extension"], import_string(extension)) + + result[extension.identifier] = extension(environment) + + return result + + +def _environment_config_check(environment: _env_bound) -> _env_bound: + """Perform a sanity check on the environment.""" + assert issubclass( + environment.undefined, Undefined + ), "'undefined' must be a subclass of 'jinja2.Undefined'." + assert ( + environment.block_start_string + != environment.variable_start_string + != environment.comment_start_string + ), "block, variable and comment start strings must be different." + assert environment.newline_sequence in { + "\r", + "\r\n", + "\n", + }, "'newline_sequence' must be one of '\\n', '\\r\\n', or '\\r'." + return environment + + +class Environment: + r"""The core component of Jinja is the `Environment`. It contains + important shared variables like configuration, filters, tests, + globals and others. Instances of this class may be modified if + they are not shared and if no template was loaded so far. + Modifications on environments after the first template was loaded + will lead to surprising effects and undefined behavior. + + Here are the possible initialization parameters: + + `block_start_string` + The string marking the beginning of a block. Defaults to ``'{%'``. + + `block_end_string` + The string marking the end of a block. Defaults to ``'%}'``. + + `variable_start_string` + The string marking the beginning of a print statement. + Defaults to ``'{{'``. + + `variable_end_string` + The string marking the end of a print statement. Defaults to + ``'}}'``. + + `comment_start_string` + The string marking the beginning of a comment. Defaults to ``'{#'``. + + `comment_end_string` + The string marking the end of a comment. Defaults to ``'#}'``. + + `line_statement_prefix` + If given and a string, this will be used as prefix for line based + statements. See also :ref:`line-statements`. + + `line_comment_prefix` + If given and a string, this will be used as prefix for line based + comments. See also :ref:`line-statements`. + + .. versionadded:: 2.2 + + `trim_blocks` + If this is set to ``True`` the first newline after a block is + removed (block, not variable tag!). Defaults to `False`. + + `lstrip_blocks` + If this is set to ``True`` leading spaces and tabs are stripped + from the start of a line to a block. Defaults to `False`. + + `newline_sequence` + The sequence that starts a newline. Must be one of ``'\r'``, + ``'\n'`` or ``'\r\n'``. The default is ``'\n'`` which is a + useful default for Linux and OS X systems as well as web + applications. + + `keep_trailing_newline` + Preserve the trailing newline when rendering templates. + The default is ``False``, which causes a single newline, + if present, to be stripped from the end of the template. + + .. versionadded:: 2.7 + + `extensions` + List of Jinja extensions to use. This can either be import paths + as strings or extension classes. For more information have a + look at :ref:`the extensions documentation `. + + `optimized` + should the optimizer be enabled? Default is ``True``. + + `undefined` + :class:`Undefined` or a subclass of it that is used to represent + undefined values in the template. + + `finalize` + A callable that can be used to process the result of a variable + expression before it is output. For example one can convert + ``None`` implicitly into an empty string here. + + `autoescape` + If set to ``True`` the XML/HTML autoescaping feature is enabled by + default. For more details about autoescaping see + :class:`~markupsafe.Markup`. As of Jinja 2.4 this can also + be a callable that is passed the template name and has to + return ``True`` or ``False`` depending on autoescape should be + enabled by default. + + .. versionchanged:: 2.4 + `autoescape` can now be a function + + `loader` + The template loader for this environment. + + `cache_size` + The size of the cache. Per default this is ``400`` which means + that if more than 400 templates are loaded the loader will clean + out the least recently used template. If the cache size is set to + ``0`` templates are recompiled all the time, if the cache size is + ``-1`` the cache will not be cleaned. + + .. versionchanged:: 2.8 + The cache size was increased to 400 from a low 50. + + `auto_reload` + Some loaders load templates from locations where the template + sources may change (ie: file system or database). If + ``auto_reload`` is set to ``True`` (default) every time a template is + requested the loader checks if the source changed and if yes, it + will reload the template. For higher performance it's possible to + disable that. + + `bytecode_cache` + If set to a bytecode cache object, this object will provide a + cache for the internal Jinja bytecode so that templates don't + have to be parsed if they were not changed. + + See :ref:`bytecode-cache` for more information. + + `enable_async` + If set to true this enables async template execution which + allows using async functions and generators. + """ + + #: if this environment is sandboxed. Modifying this variable won't make + #: the environment sandboxed though. For a real sandboxed environment + #: have a look at jinja2.sandbox. This flag alone controls the code + #: generation by the compiler. + sandboxed = False + + #: True if the environment is just an overlay + overlayed = False + + #: the environment this environment is linked to if it is an overlay + linked_to: t.Optional["Environment"] = None + + #: shared environments have this set to `True`. A shared environment + #: must not be modified + shared = False + + #: the class that is used for code generation. See + #: :class:`~jinja2.compiler.CodeGenerator` for more information. + code_generator_class: t.Type["CodeGenerator"] = CodeGenerator + + concat = "".join + + #: the context class that is used for templates. See + #: :class:`~jinja2.runtime.Context` for more information. + context_class: t.Type[Context] = Context + + template_class: t.Type["Template"] + + def __init__( + self, + block_start_string: str = BLOCK_START_STRING, + block_end_string: str = BLOCK_END_STRING, + variable_start_string: str = VARIABLE_START_STRING, + variable_end_string: str = VARIABLE_END_STRING, + comment_start_string: str = COMMENT_START_STRING, + comment_end_string: str = COMMENT_END_STRING, + line_statement_prefix: t.Optional[str] = LINE_STATEMENT_PREFIX, + line_comment_prefix: t.Optional[str] = LINE_COMMENT_PREFIX, + trim_blocks: bool = TRIM_BLOCKS, + lstrip_blocks: bool = LSTRIP_BLOCKS, + newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = NEWLINE_SEQUENCE, + keep_trailing_newline: bool = KEEP_TRAILING_NEWLINE, + extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = (), + optimized: bool = True, + undefined: t.Type[Undefined] = Undefined, + finalize: t.Optional[t.Callable[..., t.Any]] = None, + autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = False, + loader: t.Optional["BaseLoader"] = None, + cache_size: int = 400, + auto_reload: bool = True, + bytecode_cache: t.Optional["BytecodeCache"] = None, + enable_async: bool = False, + ): + # !!Important notice!! + # The constructor accepts quite a few arguments that should be + # passed by keyword rather than position. However it's important to + # not change the order of arguments because it's used at least + # internally in those cases: + # - spontaneous environments (i18n extension and Template) + # - unittests + # If parameter changes are required only add parameters at the end + # and don't change the arguments (or the defaults!) of the arguments + # existing already. + + # lexer / parser information + self.block_start_string = block_start_string + self.block_end_string = block_end_string + self.variable_start_string = variable_start_string + self.variable_end_string = variable_end_string + self.comment_start_string = comment_start_string + self.comment_end_string = comment_end_string + self.line_statement_prefix = line_statement_prefix + self.line_comment_prefix = line_comment_prefix + self.trim_blocks = trim_blocks + self.lstrip_blocks = lstrip_blocks + self.newline_sequence = newline_sequence + self.keep_trailing_newline = keep_trailing_newline + + # runtime information + self.undefined: t.Type[Undefined] = undefined + self.optimized = optimized + self.finalize = finalize + self.autoescape = autoescape + + # defaults + self.filters = DEFAULT_FILTERS.copy() + self.tests = DEFAULT_TESTS.copy() + self.globals = DEFAULT_NAMESPACE.copy() + + # set the loader provided + self.loader = loader + self.cache = create_cache(cache_size) + self.bytecode_cache = bytecode_cache + self.auto_reload = auto_reload + + # configurable policies + self.policies = DEFAULT_POLICIES.copy() + + # load extensions + self.extensions = load_extensions(self, extensions) + + self.is_async = enable_async + _environment_config_check(self) + + def add_extension(self, extension: t.Union[str, t.Type["Extension"]]) -> None: + """Adds an extension after the environment was created. + + .. versionadded:: 2.5 + """ + self.extensions.update(load_extensions(self, [extension])) + + def extend(self, **attributes: t.Any) -> None: + """Add the items to the instance of the environment if they do not exist + yet. This is used by :ref:`extensions ` to register + callbacks and configuration values without breaking inheritance. + """ + for key, value in attributes.items(): + if not hasattr(self, key): + setattr(self, key, value) + + def overlay( + self, + block_start_string: str = missing, + block_end_string: str = missing, + variable_start_string: str = missing, + variable_end_string: str = missing, + comment_start_string: str = missing, + comment_end_string: str = missing, + line_statement_prefix: t.Optional[str] = missing, + line_comment_prefix: t.Optional[str] = missing, + trim_blocks: bool = missing, + lstrip_blocks: bool = missing, + newline_sequence: "te.Literal['\\n', '\\r\\n', '\\r']" = missing, + keep_trailing_newline: bool = missing, + extensions: t.Sequence[t.Union[str, t.Type["Extension"]]] = missing, + optimized: bool = missing, + undefined: t.Type[Undefined] = missing, + finalize: t.Optional[t.Callable[..., t.Any]] = missing, + autoescape: t.Union[bool, t.Callable[[t.Optional[str]], bool]] = missing, + loader: t.Optional["BaseLoader"] = missing, + cache_size: int = missing, + auto_reload: bool = missing, + bytecode_cache: t.Optional["BytecodeCache"] = missing, + enable_async: bool = missing, + ) -> "te.Self": + """Create a new overlay environment that shares all the data with the + current environment except for cache and the overridden attributes. + Extensions cannot be removed for an overlayed environment. An overlayed + environment automatically gets all the extensions of the environment it + is linked to plus optional extra extensions. + + Creating overlays should happen after the initial environment was set + up completely. Not all attributes are truly linked, some are just + copied over so modifications on the original environment may not shine + through. + + .. versionchanged:: 3.1.5 + ``enable_async`` is applied correctly. + + .. versionchanged:: 3.1.2 + Added the ``newline_sequence``, ``keep_trailing_newline``, + and ``enable_async`` parameters to match ``__init__``. + """ + args = dict(locals()) + del args["self"], args["cache_size"], args["extensions"], args["enable_async"] + + rv = object.__new__(self.__class__) + rv.__dict__.update(self.__dict__) + rv.overlayed = True + rv.linked_to = self + + for key, value in args.items(): + if value is not missing: + setattr(rv, key, value) + + if cache_size is not missing: + rv.cache = create_cache(cache_size) + else: + rv.cache = copy_cache(self.cache) + + rv.extensions = {} + for key, value in self.extensions.items(): + rv.extensions[key] = value.bind(rv) + if extensions is not missing: + rv.extensions.update(load_extensions(rv, extensions)) + + if enable_async is not missing: + rv.is_async = enable_async + + return _environment_config_check(rv) + + @property + def lexer(self) -> Lexer: + """The lexer for this environment.""" + return get_lexer(self) + + def iter_extensions(self) -> t.Iterator["Extension"]: + """Iterates over the extensions by priority.""" + return iter(sorted(self.extensions.values(), key=lambda x: x.priority)) + + def getitem( + self, obj: t.Any, argument: t.Union[str, t.Any] + ) -> t.Union[t.Any, Undefined]: + """Get an item or attribute of an object but prefer the item.""" + try: + return obj[argument] + except (AttributeError, TypeError, LookupError): + if isinstance(argument, str): + try: + attr = str(argument) + except Exception: + pass + else: + try: + return getattr(obj, attr) + except AttributeError: + pass + return self.undefined(obj=obj, name=argument) + + def getattr(self, obj: t.Any, attribute: str) -> t.Any: + """Get an item or attribute of an object but prefer the attribute. + Unlike :meth:`getitem` the attribute *must* be a string. + """ + try: + return getattr(obj, attribute) + except AttributeError: + pass + try: + return obj[attribute] + except (TypeError, LookupError, AttributeError): + return self.undefined(obj=obj, name=attribute) + + def _filter_test_common( + self, + name: t.Union[str, Undefined], + value: t.Any, + args: t.Optional[t.Sequence[t.Any]], + kwargs: t.Optional[t.Mapping[str, t.Any]], + context: t.Optional[Context], + eval_ctx: t.Optional[EvalContext], + is_filter: bool, + ) -> t.Any: + if is_filter: + env_map = self.filters + type_name = "filter" + else: + env_map = self.tests + type_name = "test" + + func = env_map.get(name) # type: ignore + + if func is None: + msg = f"No {type_name} named {name!r}." + + if isinstance(name, Undefined): + try: + name._fail_with_undefined_error() + except Exception as e: + msg = f"{msg} ({e}; did you forget to quote the callable name?)" + + raise TemplateRuntimeError(msg) + + args = [value, *(args if args is not None else ())] + kwargs = kwargs if kwargs is not None else {} + pass_arg = _PassArg.from_obj(func) + + if pass_arg is _PassArg.context: + if context is None: + raise TemplateRuntimeError( + f"Attempted to invoke a context {type_name} without context." + ) + + args.insert(0, context) + elif pass_arg is _PassArg.eval_context: + if eval_ctx is None: + if context is not None: + eval_ctx = context.eval_ctx + else: + eval_ctx = EvalContext(self) + + args.insert(0, eval_ctx) + elif pass_arg is _PassArg.environment: + args.insert(0, self) + + return func(*args, **kwargs) + + def call_filter( + self, + name: str, + value: t.Any, + args: t.Optional[t.Sequence[t.Any]] = None, + kwargs: t.Optional[t.Mapping[str, t.Any]] = None, + context: t.Optional[Context] = None, + eval_ctx: t.Optional[EvalContext] = None, + ) -> t.Any: + """Invoke a filter on a value the same way the compiler does. + + This might return a coroutine if the filter is running from an + environment in async mode and the filter supports async + execution. It's your responsibility to await this if needed. + + .. versionadded:: 2.7 + """ + return self._filter_test_common( + name, value, args, kwargs, context, eval_ctx, True + ) + + def call_test( + self, + name: str, + value: t.Any, + args: t.Optional[t.Sequence[t.Any]] = None, + kwargs: t.Optional[t.Mapping[str, t.Any]] = None, + context: t.Optional[Context] = None, + eval_ctx: t.Optional[EvalContext] = None, + ) -> t.Any: + """Invoke a test on a value the same way the compiler does. + + This might return a coroutine if the test is running from an + environment in async mode and the test supports async execution. + It's your responsibility to await this if needed. + + .. versionchanged:: 3.0 + Tests support ``@pass_context``, etc. decorators. Added + the ``context`` and ``eval_ctx`` parameters. + + .. versionadded:: 2.7 + """ + return self._filter_test_common( + name, value, args, kwargs, context, eval_ctx, False + ) + + @internalcode + def parse( + self, + source: str, + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + ) -> nodes.Template: + """Parse the sourcecode and return the abstract syntax tree. This + tree of nodes is used by the compiler to convert the template into + executable source- or bytecode. This is useful for debugging or to + extract information from templates. + + If you are :ref:`developing Jinja extensions ` + this gives you a good overview of the node tree generated. + """ + try: + return self._parse(source, name, filename) + except TemplateSyntaxError: + self.handle_exception(source=source) + + def _parse( + self, source: str, name: t.Optional[str], filename: t.Optional[str] + ) -> nodes.Template: + """Internal parsing function used by `parse` and `compile`.""" + return Parser(self, source, name, filename).parse() + + def lex( + self, + source: str, + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + ) -> t.Iterator[t.Tuple[int, str, str]]: + """Lex the given sourcecode and return a generator that yields + tokens as tuples in the form ``(lineno, token_type, value)``. + This can be useful for :ref:`extension development ` + and debugging templates. + + This does not perform preprocessing. If you want the preprocessing + of the extensions to be applied you have to filter source through + the :meth:`preprocess` method. + """ + source = str(source) + try: + return self.lexer.tokeniter(source, name, filename) + except TemplateSyntaxError: + self.handle_exception(source=source) + + def preprocess( + self, + source: str, + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + ) -> str: + """Preprocesses the source with all extensions. This is automatically + called for all parsing and compiling methods but *not* for :meth:`lex` + because there you usually only want the actual source tokenized. + """ + return reduce( + lambda s, e: e.preprocess(s, name, filename), + self.iter_extensions(), + str(source), + ) + + def _tokenize( + self, + source: str, + name: t.Optional[str], + filename: t.Optional[str] = None, + state: t.Optional[str] = None, + ) -> TokenStream: + """Called by the parser to do the preprocessing and filtering + for all the extensions. Returns a :class:`~jinja2.lexer.TokenStream`. + """ + source = self.preprocess(source, name, filename) + stream = self.lexer.tokenize(source, name, filename, state) + + for ext in self.iter_extensions(): + stream = ext.filter_stream(stream) # type: ignore + + if not isinstance(stream, TokenStream): + stream = TokenStream(stream, name, filename) + + return stream + + def _generate( + self, + source: nodes.Template, + name: t.Optional[str], + filename: t.Optional[str], + defer_init: bool = False, + ) -> str: + """Internal hook that can be overridden to hook a different generate + method in. + + .. versionadded:: 2.5 + """ + return generate( # type: ignore + source, + self, + name, + filename, + defer_init=defer_init, + optimized=self.optimized, + ) + + def _compile(self, source: str, filename: str) -> CodeType: + """Internal hook that can be overridden to hook a different compile + method in. + + .. versionadded:: 2.5 + """ + return compile(source, filename, "exec") + + @typing.overload + def compile( + self, + source: t.Union[str, nodes.Template], + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + raw: "te.Literal[False]" = False, + defer_init: bool = False, + ) -> CodeType: ... + + @typing.overload + def compile( + self, + source: t.Union[str, nodes.Template], + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + raw: "te.Literal[True]" = ..., + defer_init: bool = False, + ) -> str: ... + + @internalcode + def compile( + self, + source: t.Union[str, nodes.Template], + name: t.Optional[str] = None, + filename: t.Optional[str] = None, + raw: bool = False, + defer_init: bool = False, + ) -> t.Union[str, CodeType]: + """Compile a node or template source code. The `name` parameter is + the load name of the template after it was joined using + :meth:`join_path` if necessary, not the filename on the file system. + the `filename` parameter is the estimated filename of the template on + the file system. If the template came from a database or memory this + can be omitted. + + The return value of this method is a python code object. If the `raw` + parameter is `True` the return value will be a string with python + code equivalent to the bytecode returned otherwise. This method is + mainly used internally. + + `defer_init` is use internally to aid the module code generator. This + causes the generated code to be able to import without the global + environment variable to be set. + + .. versionadded:: 2.4 + `defer_init` parameter added. + """ + source_hint = None + try: + if isinstance(source, str): + source_hint = source + source = self._parse(source, name, filename) + source = self._generate(source, name, filename, defer_init=defer_init) + if raw: + return source + if filename is None: + filename = "

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 T)4bs{07dXu@Ia z#~Qk!JNpW^%3B0@LsQ^BOD8=aHwxVR6GwrY=W~}m*-=jBCt7l=?Y7)@yDdl1Zp(3O zAK~%k>aj?7-jHuCq>Vw@z8@-~hR}YF#@DLdoofVEMMXD|qrJ@9G&*E|g%0X2-y)HLFCFCh!roa>>g6tYyg8Z=Pz6DTx7 zyCUKD^bcyH{ZmZ1d1lexxP5nAhL?Sp>HDGBcbSPdJLR&hISssA`}}3>J5TX_k+r*2 zXPyR7mU{L9GH}GxUtk-Dd{BsWEw8-nQj(c*G$v2s0x>L~QpZcW1| z^u=x=SJ4PHCW&*HnF2fsU8Cmg^FR+(RkVkyj`IMz{!1;g4O36ujq?DVr}$L&0piaC z=)&+ifVd$5pvDMPeT3>hLat^K@0c=~YZ{>jra>Q}s*jMXZmJ*gh8piw8uSsW92)r0 zxJ>>}x#C%1*i&xQQ*JzqU8rq$rl;I&PrI)Ecm2fqSK}wom-8jX)igrQ(WDV-2vvPF zxtfQ@^Wu_kV4@Lfn3LakQVmc;sOlqB>ql{ZN%QQ1O66n|e`Ed*`dadzC~dd#g#gfY z+gn~00)CCsc3VCaQ*eBoH<=hXVjK3MVk@>8IAU9lV?4sjDaqT;)A6v7HpXE*gG#6& zG~;OerTEHyksFp!aXOyciYBOGZc87b;Rq~!V&R5JLuCx6L=Gu*8z{#G0PEm5o4nI9Q zUI-0b51}dwJ+WI;9nGQ9aCtbU5o!oIhnXqBlhE%eCi}SALsb>+p{nD!iLSHNBBwm+ zsk?F9r1KP?>OMgHaT8q_UI!321OU_+fvS&C-AAasJUv!7-LsU+tZlwn@wFPxH^#6A zK40I%d$W+|3()xXGx&V14sYCo&sXH|b}RV#teS>Z7Wta%s~m5;Eyvt$%W=2ca_sH4 z9DloQoj#ntEgXba4Ig5g;X`aQe28s^53$YgA-3f{_zdUo7d>aDVQr6-5$~8!M!aL2 z5%1V$#5=Yb@s4f87P^KHahwbvVw>SZY%_d_ZH5oA&F~?%fgL)UAyc3d{Z8r7>xtQV zBs+fYhOj{R#Q46=@+z&l`+B+x>`8(YFN&^%M zx?Mj8bOWUFwEn@2AEeU1U3MkhU*HKQx7}{b?YG-<4DGgrAe5%y zxMGVgat4mrX5ffz29DU4;}|OA`W}ZayX}}6{>L#g{Euyh|FNyF|AHgN%)k-b3>>k| zz!BSW9An1_JXdEz8$mAZ>n~6_W{%8tzut&j;-l*?ec7-z1ywf29aAUlNs!{dN7izl z$>z%ORGo@{taQpL!8x#gRk1U#XLY?`jfVeuJ$n3?Qurq;NF0XGIs$U|c@)xGmLrl} z`XH-Aj;h_3BWt(i=-O>L!ggDU5_DH6+Dz?vP!kd_?P4WqiI6nQ=wWOZ3ZqKdlu0!TLJW`AXr( zZ$8#H{lUMd)_*zU2h{1`v@Y2GNe=#i)gkw>-In{=Zp(dbx8?q}+j5`VZMom=w%qr2 zTk<@V*7D!~AY$&f=jHx-Uhb#o<^FkI?w9A~{&-&Qhv((^XDc$Y){GQyJ6{(W7t+SO zn4Lo<)DW6;G=3*e_2QP>5-MI%+PWQ0P{Z7|K0?C^EPZ0(hDbxFMXuCspd1$ftQT9x zGZ=)*r2XkI&qNvNQqnLw<4}Xwy@V`eb5Ye#rwuCh5VBM`M{7b=485ypgmy(DyaeRj zR!#B)mCDA!410C_TJ(8}ETZxq@XWbe$JgtNug48GN(;+aNa~hLbw%>a_{q8R5=C`c zLPzlvk+pJ&qDtsi@`>J7mU=FiJqsZIew}z7k>W=2$jB-pQpbJ>$*=LebWgDe`J2p* zSs2EC(=GUGmyKC)Lp0wjI$DINay^9PBU!59*ltb3D0FnUkgI5f8dKIe%uE5Egzl;4 z?3d$usFGQ%Dp1w&avWlJQ*mAeqn^4OFUQe&icfVPAU@^Lh2eDoaYFz=jS;B&2-SUr z<^*l{!`w;?UU<2_Gu|c)o#m?wcGO3OuH>V)wJ7)60{p-Scr0b zftw=>+#FNj=4b*J`_Qcv+%L-bbt7uWGmlTZEss*WEss^ZEst2cErv-8pvgvX((R zh@ZeJcA9}ZSLOdK_AB>#K?_PAG(%$Mk0N%u`lDptUxJc%HsX{YLb3jhY`NO5kF%&` zx#BUGUk8HEuLSWELGVBJYCkg0eNicVj#`|v;Ij^Yu!}D_dvlSWnEX_#qcFN;!E^X}cvvUYR4WT(l?PutYW?LI zpC8WUi{jDq(^mz1CuPsl=V<-+GydzfPHv&+hswX}YbNn8YW-mu|M^;fUB(Z+)4%3MksJui=(=j9ReygX8#mq*C+^2m5z9ud#WBjI^@1ZFE{-HgsUiPw~S zfn3@+X)n~SO{O(sKCDP*Z~4iyPiG4!sY5!w}r zbT8oC{)+9Oh;crj$+BlNLAU9mE-Z@d6SA@8Q&w2{Ofx@Tw+-UOESD%8eT39 z7ZXZmzig#9R+kP=u2H;4WUZ9y$^%48S?bvbh~LnO|83JaP`q7a6-TT(_Cv@%ocPDN zjaeARe$y@Z8-$Hna6>fza&)v9tIG8ds-w^e-I|6`=#Je&uA&iY%w6X&GX;1Ox{sQ( zZxDK@lCNM@6{zaCK|t)DD$avD_0-+CL7?*#pX6&^*9PKK4lxX`1Be>}0BVdt)kmoA zBb1(mG56~+c;N;iKg)2=&Cew8^K%LO{2T&b!Zq>Nji`Nw%Tcx4a%Anc99_FDN7!zg zfGCx2lwl#t@da*Fx8)IQxAl*k z?9@3pVS(w5acf-4p@`HA^cn6#rN*wm-7!!P(1~UUd9)ALX6BPwgRF^XDZyhy9r~?=L~gI~#E-PrKOv zcD9OrOK%TQ%IdRMZwDB1_}m)s>0$WX0+0{ziQzXiyzp3O+#o|k(yTan#=qS-AmnDUgK zOB?fhb`Bw^AvEV`JX`Jd6e=Fowr)og)X=o8kI=9JOP^S{A=1!kkt=l@D8~f=>j8E= zgF&cF=<0|vKjqbXB%p@T8Al^t_Y$&@%|%tuoHnS~L&#F)9IXjeG4!sY5!w}r^jv(t zBD)|WoM$fnm9E>eD>Cig9(T5Rimb-6b>tJ#*iA58nq4lk%T=@xPojZkB9JBOJmz?0DX z)tvq8-9uFs?V+mU*&AK&Rf|0GQcvBDXKy-B@u}_u#HSp(FuV>RZU_LVF#=T|p}LPy zdWOW@mt*k4vv+dLqb7eXHGXSJF6DBBhm9~@sv?|AlpxC*Q! zL5k0atYy&hoM$XF1NS0z_*Kzy^Vu7eJoAQZ&7W@U9QG&CyuSo9?<}l*6kUBkJ*xa_ zJhBv9sH~8jNzeoPLFdkb`-ZLt9IuP8vzAjcQDIkLdbF$HdpCUA2c{!knK@@Ns? z90~j!3;Y}n{2UK_>_fW|wd0wiYPaRc+HEUJ{?@ zmu{}#?Q@7y(RLdx-afIusb4yBbo>#^Vuu>c5H~PinXQBXf3*T)27u}v*?Y%w9LVQj z;A0;eA_kfZ&%ljOeaG_kP+h}A4u6mM2 z11c$VyE_SLyoSP$o>4G#$P}j=)SC5mHN+~kUr+ydJ#_S8JmtI28Oq&pV($XLiz3VX zp-xL(jA~MM8tRy-#%yTNZOPG%C41W0hEDh>hIZ%FNW33!p7&kqHt2nqy7_W!=J7R+ z%_(G6qEFKV{e7jzSK2#Q3Kff#b2F~Q&?i6ao_$im?%Buhft%eRTDxZ-w06%vXyxWz zN2j&Kr6Hu03(3Lrygg%Dp;c7&CvnJ&J)#t{mC1F#DuEg+QRQd^{@6XanXO=ov_b;f zy)K@kD-P5!>;^PKjlL1iUJf-Z&Nv#;*i)!@NoH;< znxKZJEq#Q7Yn*3Z71L*)gV7J-SH#UzOb54e$&hnWscac#sb`U%k}rdwsjWJm4Cg7{ zI{#*bgCjf;ewa%x^9DJ#1qJhOysN1O6##V+Pz1 z#aBi*ivU%whfp1b-rcQf7=_;6E#xX1p+*KfhnXqBlhB9Noc)qX4^>sPhpLX3OwjcK zwa6RZU_LVF#=T|p}LQdpC+HvaD7#2Gm$tK@`!29 zEpW+`oWu%l3Ez0=?TM-Ygwp{f+JRNe4ER6z|@rI4lSKm%0~{T&Bj z2P!9+$KNyEtHeAiB@JY*A_CAZbzAnsAavSbjmAhpE#u8S1{XjdjTb@lE4sfvI_|Uc z6#pW!ihCMAk4t6CC`&zy?M7lIea}$UPl+bxDZVJORxZ$DQfHh2lCsoux$Id0891Ww z8I4Bg0)*hFQ`jid*Lbz1?q|TS<{L9GjQv1idf?9w8#CaBDE?mbyoh7vdI;4~=s&tO z4WrQKyMJWZ;bR1D&V%RQCbm zGY(xCUI!321OU_+fvS&C-A5?Bb-+0c9-z;QvVb-}?8PPXaeCPde}~@7?^Fu^9=fow zUcqOfxkTIH^ZWGto-}+3reSSIKM!WREstrtEe~zGEst=!Ee~|N ztrgy;n0&~iExTO~Tv$0?VFB}AaZ_Ic-K!HTH@rPg^dH5Ipge*|uQ^=@SP$e%2oN{b zDR0~oNk=(?+(reCPJ$F45?RZjWkoQSx`F#QegDc=;`>;NbBbScL3wjU-6Z+dTKRgL znFqvg;T!Mm@0s)7GB&woVdZw60~YTtv0nfW{qXB9$(3jC;f2FbDs$jYq4=q`Tw%Jt zN-u>z6)&QSvxlo`gj(a%N2ux}VcP!d1b-*OZC{iYI(5G<8{UvGae% z&I=UVofiyv&V5rmT^l>k-$pIoNvSbk2lB-AUG1@-ipjcl@_yk-Z1sl9Lsh#ixHI|h zRT`d8#xN~X{G777{daE1zXWy1D3$P0JX2Yr`|BC-YOjNpj@%hnxxKTK zps6AJ%PK5Kk)s|Wfxt8rJ|Hep5b)8LD4KyG*CiVCBqIxFFR{?$c*O`-8C&2%g$2Sl zD?%d!oI6rgq0(5a1{HRo6F;SMs*#aPs`WRsV744?inYrn4g2!d_v*oW)`<16kTlhK z(^uTQd2o0CqEwQN;=KJy__K)*-`D9)ESRjqVaTVsY zBamp3*^eSJNm#5xH&VsI>ahwjRsKN{eom>ZhZL`gtR>9eAo{U{*)QmVZ=Mh~8i9cB z8>;%-$pppi(b?tF+47M@^r4KNvN4^-)laUX-A16Gqd2HPiSaQE61*f-Jje}P5231` z5N2lMhEVs^e#4})Gfsy^imWeOZ*@{*H(r)%TmqmVJIk6K&EYyBM+TOS6$=KSu4dInF{yMvt4-f4o0 z_P@t!^NDfq>DU!HnBFw%lOsBnTg2Xr6mQvd>{NOp~G8Z4~^`$%)xjDtAoIQY4Dz{B(}so)mi)L9U|^1KhrIDV&MYy;LG(UA_#X zDh$h6DYxg`&-9jTiyoJ^xG(J2agQpS0@8=7w7FL$F_iAtQaV1od@fFRYg?x&F2zij zj!|@}H|ef1y;e>7?Q&$hln1dWIMjQl_#}%u-hMQ&x(Z%7(ZHHId1drNO>s z40`9ydJ<+(+G{xjM2fcC-mH`DPC7L0wq@lp5!-F;H1uV849MVGOTQ&G{odU<+&@h9 z%lb(10L?s^uea6WN@AU*MM5G{c_f6D2S8Z4-@+>0^_(ft4v%Pe{k`HK{D!))z;Bwm za2da;>QIJXKC^SjtZesTqR*XTBFnw|CE4hh2N{1EzwzEas+-%}dzb_7@)~dNlf2%s zN&*r(@wz;8c&|6Nx5t>9wsC{@feCeaFJ8G`Vs$^U;SAcK<~e!PPi)*zY^EPJpF)46 zp;(Pe%kq)BGO}tK5AB)B9ny0Ds+u5$HMBd3JJL8?>7|TQL_d}=`?t~TJUZJH?CXm_(C}eZ{g256#s7?~<ZrnMP(NlJ$cp0CoXt%YU z@#Rq*)V(o2hCzatgp6nR9zvDtC4`xaBBsLJ(B(pMcsRK@Bb8#tZOW*hu2RgzG3D78fFfm zVFKc4T|XKol{+d^9*iimzBF=^@Y=O^YJ5H)2%DYUzrG-DlKXzp3)}G5> zQr=Dl(r$aW@&MUx1L}tra=Yyox+3F7yA4dx+JO#dtYyKHDm@mR;l$Y}N8dkakmPED z;@_y#GRN@0QeTC2uoel4u<}RM77Z645R_ISap`E{yS}sSCHa zo2m|F_~lR8xnuTypmX&3Oic5$XIFn+qw+YV^8J$bTd6nR+edYCdwUOa;9Xwh?f*?) z?^q=PQ6gU3x3`_pV+vc*i1icVPYPm z>kJF8?4_(X57hJ()~UK&{zg3878inC>H1}a2%}t<1(dZ`EY@tONKIL>j)ZB>KDySWsDw9VWW$LAzanK+2R94Cv%EmpFm2!r%G+xBwsWB$;O!1S%x{ap((Qyha9jirV zhfK6`B`6LM$*?cw#zU-GW3V5%IfTH?Q3NhP9--?(;8Hs+t`>_F$?YIG2{P4oYe1eI z1G5XP*D1^IE*!w_9)q=u;(e55Z9-KH2X<^Ubs=XAuzYojB0`xVglC96IXZE+!?9~x zJc4~f^x|xXXZLFH&+owJ`}5 zi}S$s5UTnJdABgc(^PX3CpmbY>O7urZf^-&ZrP0FzM%G*lcVlu*GKCOIy^*6E&)J1 zi0hnji}G^q_nJ8N#<2d?+Cm<~n>U2PUm9Gtp`PBRyz^bR3$~B27Eogck$6x%ePY)M zO7eg2sxtRb+The7FbNl6a zJkW4-@H`&r9v+Ve$F5%c_F!y_4vm+?J~TXlg64nI`;o)SWg6qji}Uh{d>ngR(zD0R z8a>J{WRl7_Ym>%({DQ*#>^-T7GP6f`;@DH7~+w zGA=Kh$OlG)b-*5Z#=_J0s@4xF9l3Mm>3x+dX(87q6v3gAIS`Qq1vw!siU8BjZ6v;2 z&4LGNdr*&QhuU((o^qp}a^s%(!smOyM#T1g}dorNY3?7KffgYqk>0ifQ!!QPT&GMm@=)|gW65O>g`=$wy=W(UB?b$ zkm##*b0T&Ujno)Q<2lwb=vr&z>>54FJ$!B(x}j^?*t+f>!|`y;Y=?#f!Y0(71>pzM z_LGIMst?tb{V1ig`kN+uXW`slTegIj!)I6b&ceHM(F@D3)t3DyJ~DXF-r4K4VH6Lk zPjScJ`pNPWTYG2WLAN<=YmDZ@={K%zjsW~bXOz5~=h8h?ZluQK@&RBRF{}gVww69z zn+)vi#1SY*Bq8-dm48BT`(n!nVeHOnepn_Sg4w{AXygMx56jCu;{sOLySbiSZr{)W=P&Fyw1qbSa`;!Un0A-B+#TEfrs*jjwX<^u z*YBgPWxYNQ9l7)0xo7Vz$Nxig4m7eae}DRC%UszB!MF#RYv>Y?%4LICpwWPnBB0Paa~&~^T&%5Z&pkr?-kMIP9gY_-xk>3 zg{b5j1QFdo7w;7pDRTc{;P&UOT-kcygh$w1!-nBh&CScEsdFPj47Gz=^^~i7$_;zU zg~8VJ86d6R-OATU0cpnYTWC~ z_tt9TIo(17_tdq8LWKWyOUTo&Z4GqbR(`0(U)~;B=Ux`1pQ}`)0>k?E8>;fZmCBKt z;wz(--B`J78&-DDsKI1-cWHQcZnD-ZNzA@PZTOQ~Zp8C)E1s8|u}*n%KFciORm(w) z58UQCINXA3Q2C~+Xnqs4VV&i^d zGyTN2Yy%V2#1^jh5IM#it+@@1Ry(3U)L34pbR<7vD_46|4~z@NJVkJ*BvTt8G6@;R zo5qD}o+4?ao!dx!xtawJ)XukhOgq$;lLu|}3u?=addiJ^;tN-B*d$`Xb{??6C1wQ3 z#@W8wLpwwD^Bp;VH3yn`wwbz5@o=V~v?yJ!7&^Z5| zAvboXYwdAL(-k3{Cu&RkiV)uK7QH}pXxw?cl@+Z$T4}h83ipEWpndKI@#T26FDLqo z_E5n=c?mv8W2Kj zGh0jdyIim#y2-Ep_m=5I=Rrqqw}I?@`Z3zUL2Wnp-JTeW$J|{RUUnt zyUV-lZgo$k9T%FfQB{{JH7_)K2)P)QXX#33&!?)!d#Ei}k7$QBt5ZH0^_XU;h`r9Z z(4-k^Tb0{zML*pOc?D#Ha>l!@N@e~~W#p1~9$Z`u1V1>EFzB{-A$Yk4K}7cx;)Bj2 zMKl_?{V6k7wjMa)VLR8bVL0JJbJNth5g~@!L9Keq)jj2gJ>|k+a?y02UZe$#)Sely z*A^*0YeFz6N&i+g3Zb^s^@w(8$LypT+A%w6hT2wV97DmqpKJo#j;=pLm?Keu0C$S==Ryhd5I+jH`2wSQ>;l2VbQcu)i(a#Zf9Op&{r z7Efx+QNaU|%kc{AHDjNd%PUD4|m9K0+#xbnWj=M~($U371y z^kk*HD@k{X0F@US za-71-VG1ipDXbi%uyTyT${`N75G<&9M>?9J8ET4+EwS=0V5XnZxqf0g@!TJ4I^JAI zJ*?74Ywg$xE>Zq%&B!2|2ijievFD;ihOvnW6=29hbkJ&!qG9GB8fK26VdkS&)i?_^ zhojy?Gt?9t^%EQS6PxKLwq+ZbpeD9(IgiNub%?{j5WfR9Fo^Al{;6s`U+Ks_!(Yx* zeVwvpzET8-O6EXBl41Na)Leo0x{xsAk^t6A_s?KG^%v_oyVVNbbHPq}eVeBlxu zY(#9|N3MN2k9LOY=R0!#avel(oyb(OfuXlYDqYT_rN3X;(+FUYoRf^C2!@XI^Q+rS z6g)};Ty&zqcEYEy|3e2;rZsy|yD3<&Ph0qe1AfoXqhg%{oV!}*Jh78#G*N2(<1go# zCD3N^KS29J%^ksC&eIDjf`_is3)(PSW}BkFoTnGGziodxPcLZy-u`l)UeJ(QK!(~0 zH+eaahA+qYcdlHUJKg-}l%~shINP%^+k^67ww^fgK{B}qp?!ZU(TZseuDBTk=-QX_InofFlQb$1nutzr*UrplYGs*+H@Sv{JfEAk7QML6vMOllzMS8D zAN{p@u66xB+FCZ?qg>B(?mko_->gKO{-F(%!~gGS6dPt9{@VFl`^{E51PHW}6(l!3 z9McNjA}jj%T>C2Dx#3te=w&|M^Wf3^W(sPc-0-Xa=fybq8-A_4I5PbWzg8|#$_>9( zdDL$%^LZY$u@3x?EWVKl1ws+q)3FT!SE@`>o?G&?3cj zF;sj?-5*tRW$S?x9^`Wk8-^2Z_%}_R8xdlt9n`9)T-{S{*i$YHCRa=^n7Fv7_TqHI zPw^!ag86i+Y7|0kr|S{zP;++HW16An>@3A>t22(F;GR>q4C{s;u?MF;s1$RKX0(|% z{5^zX6eXh{rY1iZP478zl?pe|audv2&GU;~N}ssO;|cRoNiqMV7?3kgT%~fBG9~gQB9WU^rM1B$wAa^T z@8!I=eDDF_M6TmQ7s&)_0Vfw?2`SNQA$^bTij5o<##7|-a0&}d1XEafD20_EQIf&a6dn_I!g-1*qQ!>-nw@O$Mb-S>Xd$M+ z4Wqg^-uFr@g&4`ep{kYPv9(V~y8NQXszYFQNQnrE-X+c;Y8QlT0g$zpAY8CD7C!lLRR~ zc0w?jOQmmh8y+WpNuCn@5R$L=IenXdq1L%#&8r;J_+*~ZbD?P}hkaB!cO!M-Fr~7} zKemA9+`*Zcak(~dhpa8+y~ot?ZIcYqZ?u_Sy8Y*)xAETLTYd8W;t95w)wZxG0;h84 zpV~%}%13J_yxCWMUh@?n3;I8yRFZ+>mm+Hy#cwMs1P7Q7TcR z_y>`-i{kSpMuFOkk|4#uoDj?-~%H>Sy5Ggw@ucGDzC@m3Rp z)NY#uDc)s5klKBc;1!pK0582IP(efSTxA8=)SiAV}>`WA?02 zJWlf@2vU1s%$}84Yz^VUD%rDN9KscfM@|S*OJlu45gJUB0qsy)KZ0I{u+1Ty=f`mF zq(}_qX`Lbg^ayqu;as-DxpTD)=du;fovVcN-Z7lZvDk{z&KSux%{O$>xGpXp%M?d(@$g2dk@eD-B3U1d zP>W;YmMOkyLL;@mj^nsY@!uu{sSV=rEK_721=G}SoCI&BMTS^lQ)Fs|zSJ0E5v0gy z2SEn>e{{eDm)h32b1hSRK-{@}9I3GZiXC>nmrn>%V`CCQifl~5be=HN5{m?4>66`u zBH80JeZiQGaZ@3Cf*=V*G6XKQxWBmdZ#9f!(3lTo;4+pBk@!Oq8Uz_N!XCIpJ_XeK zzMg=3*)phcYiS2{>ekW@>dsXwsGGClw1c{H)e7p)Rf5Wtkj$gVlW zQ;g_O+l<nAHwEwLBGDGvu!fN;OW&Z(3EML9> z%U1t>mi@Qq?2Bcqe?QC4;pt?oeLN=r{!Sx;oOyHl_iGDzt?2k_it;=6#3W>>WTz4C zBnoP|23ZAquOR4v`pJ&IPi+8ESS)f4`+~^laIP;z11G1)DBKX|8ukT|b3|rx%!py4 zS8D{3GxOw2YYTae=oslCQ!e#SiYM$F>nW>YCj0GD-d~orf%ZBe9@pae9ED+}YwPi4 zKg#I4M&b9z6LKW)zb|;|Up(_um&h@>NqBm5I^vp_}~saVBOa$UsLNU8az{ zygR9SO6>qu$a70L#!_R`mMQWqeJJ6%B?wYu(-uL_OFX&=iz3f0p)WPgry@v^=awKy zjYk&|q{wqi5TwSViwIKWxg`ivBkM)*R^`J98!9yP<^04`M&MH8K|?fBd6&!Uilt#N`c<_FmE|r6RtniRhlaAk z%Yl2iTHLKvM$$Uc3?8HHP+N{W*{7)VA5Umy@!3D!V~8zkhsA_jVX64ggkTYghB0u4 zF*$W(jI}MKD79oPu1hUr9fx-Rt8CYcv9K3cmN5w!o)Ne(6x5PuJ;NwJ45MKt0@g<^ zGmwKGb$$D3QAM@BD_O_I~uPaaa z{_W{VzCtC(Ug7xgS?YhKeE+4%!p;%RMg^~&YRY%qM^kfA!E{q|`)xWTUW4g*`CmOk3TiE$RYlT1R5R9+s$Czc zVK1r5VTtpce5-K|e%MFIxm}9+OlDj8J^aosuProikLn>*^FRSL55lsTt1hS{wQ+D6 z&(RJQIb(Ydp&?nArD38_W1`BN9Vp&>Vshv4mAV{=KB12apHeDYB*odd3M`SI$0k;( zjt7u8MODQWpK z6WYE#>2d#PV1)=_+qhr$vE5itkME|zPuqT#UdEkMx|Kt}!RQVJk z`S6Mg{nj*~D?NlNMjL8pv2)MXOD5hQFztO|^$gO`Y0}C4^S&3#dzH${Me!4nwM6kV zk+n$io67PRj*F|*s%wH;9i55XOMoDNhu|l z2M;VSEXIX4dH3M#_;BOS!ZA)iQIOkRy<`7yx*H!}evK4kWYP2P7%mVlR4VyO@&3nh zx3EI-bY+F)q;^&kqlV1(KU$`wJ1yC(#xos|SB zzG^~{+Pjk=#r@yiHf_ag8jn~$FUE3#;;%%{mnjmxFxjY`nFJ{ky&y>KoFqu`_mvd{ zsimMV6ZH4Rpf6K=zp_GKYX6u7DKZ{GklJ@*ESD*MuNCx#Rcb#;f)v|9FW>Xfe8a4d zdh?5KzVJ#7?;DiXZhr49m0#8vG@nH5kcWMRpG?`b5fB?0flpP1BWc-SceJ=3~UFjodH;W?O80$)I$tF*T5A5 z^{p&AlD(jT3JX+!7+1vN3z$UR0MIW=VieyVS;ffv09~sv4vZ_1OiHFVTUN(-l)dF} zv-+|S_d-d#t(7ZH(Ly56bvX{>QO|eZrr7a8;zrNM#HnD$%{lh!rzJs(7ev;|sul@| zBuDN-HN-r0A`R^0(d`uZ@k#|bCf*dScOd8R@ZKay@e7f)@)a$zNE^t#CjelVYDT== zos0$BuVWkU?Jqhv-rH}|^uA9F;jSFFu?t8?`TLm8jek?xcunOqapEje{A^n;s7U{Y z9d%Pxdgb}CLsEo8$Hh8#ji<9g?1xb29Nw6Ls-FQLl#LmvW51~$_ypRR0S_%dYBfRn zwvO^xr;Zu9TCTM>-g_uF{CRJGQJkALMz(f6gv2%Z!-1`BN2nYa*N8=m=@eSG^+Z1ouvbC^E4Fn>X zo9|o~Jc6&T%g_Bqn`2@`Md0rki`xOo6(8mz9GULBe zW~9*#CBKGmtf#-L-B?fOeyV`#ltwwQK_4Oc=G0q-^UiCO)^4`k>(lFbh`;5q&`nN7 z^ZSLZWA*Qpcbxv(dAsdy%6quccEQ^M_D?Ep_q+YuL)&wdw%c-#>%y7J9ao-r4XM&& z(d~b5TmJ~o8#G9M6Vu|$)M;7j@c&DRb!`?Bk;)?>tULh1%Ka7=V4wlr6lfPmv`ag) z@EhvF7;l=oZ~?KY>QIJXej?|NS(Dobj6Uy*X@2(X>U}gS4^&z^VG>@b5A()*`>1Yi zZ|`9ayvu96y)t>dW0eF%iFm!k^m=o9dyKhh8#ib_mQa^JL@VD0q0Ljp>V9Iw8MHwq zDy7j;Ke2H?v6+6@!o=K9R}vOpd6s0oxvi$JupXn!QyYtJ-^KvP+T8O-7^)i}?x>%IGOu-&tHv2ZU20E z86rEQ6K6Xd`_L0INRabEn7VFLI?Umj|Zr(ww;eEZ!_#*k6dP)LL5mahAMCw zEYd4*$#XomMjjGT^z(?d-&15zD=;Qf?LSF0hRBU}1{H4L(4fPD3ta~$g z(!{P4l;r>3Uu7Pow8=9n4^^hbnBuRs<#J`t(fqL3bEy>a98St7CVdC3Qu(FWdy(Qj z+j6!4v_xbrFHIfsNhugjHgabMVvk4ik@(9m|i(stY9^oizaO51ID9ESc@Q2(&^0M+St z4D1}Qx;zj4+|79&4>TMdJda1ZyT#+dv8!`BBsW#+<**M84~(Gs-}HXuu(DTUyk9HE ze-4|80?j9j~ocjy);+8t*;jnXCtsOJLB^SOs!OgS5 zJ}w1&SYu4{Xk&q_&fBp?X6Nl>5VWeX2wkg3y#-0=T0KVV?lE3>kC}D%*s=}$(6#&s zKf#CeziD2C(PUhnwt@_}CXlJN?z9Z*lk#cwQ+Wh9_1cBw+-FUwQOu%cMt!QddzlcNFZ!N z?O71stK0mrUhQ;cpM|>cvs`;;;cU~E_D|=*yHU{#%YUpb`%j*$_Re0d4PS|BpW=?c zwf{}Cy|eJ3eSSrc0DM+wl)MY&(mhmupiH@Z02oIM>j3J1MGt+rHW}EjD{4Ei5>kC5 zbOwyzyO0HqUaP)WNviNLRo&1hmrpgYg{El2NIr$yAUUKp~a=qljU~>Jl zO|O3fMru2hE0dby0TY7xbgF6;LT#t(5$({9*-10BV|LOEwXMpBXEijtlp6Q?@+|_| zxZEu?aHp;<6oYeBC>^C#qDgV5vcelIsr^P0r1;wtf>Co>P`M+biUo=fimWAy81&{B zR;is91s5o`&6l4wizu|?Eu(^+6w&Q1?TnTlodhYiEtTIT>)f5R3oe)=c=tkRK4P3G zob<6!{S~D$G!)NPRv4N?FARdWQyRDvx>^;~o)*hJENt4OM(>JmaLroEA4^{W=_7L5SlMT>|atD=Q2Qxp+daoJ8$vRJ%a2t#)CR8dMT6x73l7uTj-+izeptp4~b5eEs~(-^9B6d|v@5Al`ueDJC&jkXou*PNet$NJEU~W9B0;uZ6RTdW z2%*gnVoo|EkKH4hp(0kk#^l@`j}Ok>U+EfeL{A+aaYE?We!=j3<=sS6r`>jIWoGOCAAZ#73Pii_EFv3-rmC;c$e3B`&-HD9jianG)0Mc zZNGPYb9;M?xoI0WXrGc$moHjZu9sNdPi#1YHmF3UG&<@hHtr`j(+^vin9g0M;b5)y zpHA1I{83zlR#_|Ff2`d?Hc*bM@mdmCE`` z@g0%13$xco*LJZ?zCP};9ml`_-B@`)*yi7npipFMv$p%#4JrKfAtHdt54B74Riz64lIZI>4#&RH;^Eo%TRikac`N%j zRe#&g>o?I(u*dXZEJGAYM#eC3nIw#!9dZAb^~!C&UcVpuZ^3p^_n;L;%J?Azm^=WsfHXwrAkDwR`W??sBc+H$$dszx2ud_bv3G(%+_GTxd_D^#Qo z%}v&0TA`*%=ZFLsKIY{@E;RjiCztM}(z*f~_CXgszrk_g4(xQL?YEeJOs@#9RoZTQ zye9n9l(yUQn5MU;W9{Lp(eDjd;Tq5Lc-rI4<9XcT9HP&7p2rgfQ^@mj>){~}%m15x zjvQ8QuF+iUtzzTY<0d_O%&gI)d;^k?Tx_gY*-D76m zJ+^EEKXffW!kf^MK6fG?7?&?ju)noKPZGFR@a-*Jd;hp5z zBx=ES9UaZtrv3&<$P7#@2QB z@b6D&u0unbV-sr6Zh3!t-MoQXM(y7<&%UNKyhS;@T^t^C^6lc-_>OKH#XHfRo+Rd! z&c?UC!`ZGa?YF+eyGhZDw1)b3ljHuY6uw+FfQw^>l?m;x-_Wj;5yZS5!SMETVrYQK z!y4XMeXhJGoM1vjz@6>iyZXo|}zK>q3jn0p8Umv@*tj9<3dEmE< z@M`z>bsT@Hw00|dXQ|v+=W2KtxmFHV%Dc$5%40$!%d-Mh-m?BWdCU5(bQpM8Y>e?_ zc(6VhcD&>KFtu}grE;TEj$RKTxhGN$_fu-8rF^hn+o86ce6l{1lQ*kgv?s?VAFVIN zUe`m+wmRAB(xsWuVh(-gs z&-Yx}dfi3RihAUJ6(@xhjz?QnxP_Q%}<)4wpHhL>vVs#QsctD>LcVF8vZP5IxP=|CA3ru zt(S9>F3G3HH)bqSyxCpCoDjDt9-*x8&P-~*p9Cph5Lr7ZUO3U0+Sa>8BNTTiD;S}6 z!z4)Ypb0^0H%o#P51kOCc6bt`c+`X-wWE_D#bYM~sU4pLDV{hXNbMd;km5-bg4Ccg z2D~mAxs_G2KAAH&uX1GAdibs_;~w0oh;EKJasu6;Y-S-}S z17kg?XOV!)PrMzP2WwnN$xpW(IzIfw)m+jIzof9B)}g-;s@++JF89K_hRx*7O3gQ# zhc_rAN}jY|_#>%5sF7~B5q(16Zp)pk3P9FtftFPHBAMup-&&4)f30rHEgr>}oXB>% zbcq(POsp?z5vS$NfVr>2%3UyO@>oUE~L4+eqg(Aa1<3kLu?3_8#WIyS&ERk0q~ntdavHpm=S+$$N8q zdyKhh8#ib_oluwWx2{|-vAUnwa0YEq^Sky&{lv!o#Af_}jG#432e?*Hov2t`f>o^ut8i+cT=2I zmbEigj&NYdMpGAZ#sCl8T@?|^3?aNjWFb0nw!^V^wRi;kCoLYHjiP5~qc&3B>ir@Ohs#)C7k;$ZA?PN;yfsXy7!kJ>g0obnawVBrKI7f z&pyDvL=`tjeiDwSw9e;%&Fw7^bIS&5pHCaNYxVAD*GKEw8YQA7_W~gPur?W21E#swh=lWRa zoqYdO`Pp_ixUC|AcIh6Bk$@U&!mtoBuMaE7Yhe%9E6FaaK7Zy_h8H`;E=9TkVSDn8&&MLmJNT4w=o;FL-)#Bu^J%(HAbi|gv=2d)VxXnH9{P&CjAhl zOZq=okj<+TT+{Q4Lc`=%{^NRg-!QqQ;^mWuNrn4+Ufke9n7erR0ZICIP+SvXtt|W1(rQRor~xDm>36;s%0E^r!W3`#_0S~Q zOz}Wv1=G}SodhW!F(H`DrBZ2l0mJRi)^{R}Rry@BYksxC_q$#D@93@Zp+Dy~*3(b% zjrIi4NL{ifOXYexzMm>#d}Mh)L|KGZSZOFyiJmt|k)`M1iIwED(mlgf-a$$wmna?@ zSxXeZcT$iJx%9-p4Z0q4MWA+aXZFf!fnWgye-RPvqWC6dd0Pvs)ZUc@DL(983F^Ws zwdW~U<_E-YgYuW zbRTN59d~QaH64oR?j*Wx>pNGm-d1XV581XtWlWiy#&IZ4tbQ~$*3-|?jrDYn#62Pc zD;AHck5CiJj(^ZiipnNOUe+;-mdL>0i}|wj2U>hqVm()j&rhtEYw@DQdXp9}O{~kc zctv87j-O8~a`799^*NoyUso#VAE*iVbfv-~3-5kK2!gP7YH=a4LfiCz^Jq0EA>zg4 zZJ0#tQ5wXCNribjl#9^4v(%WXjVS)7Joln_!X#=$so1^*i^hqWer8mcY%k!0Hd~Tm$@EMAoY|M!2`W_rrTOuDj+2@7qkP+lu#Rx1%BdswTySibmJ zfc!go00Jd&n1;d+@LT`N>n1EfP-GcSDEwCMrYI_g^d&LGExJK17e>zLXy|t=8Bd7o z!xD?ZOX6ZsoIC1KJ)$SHdBUVG)O_r8a%_3D+Jnkc^&GXw8M5PO?%eNbUlw*bUSFtv z-xV=0k`NzAtULgf!);Vbcypmpe@1EZl^e1tgDM*TuvGWP4XF$0@?9+~6#2{v+*8yJ zOOw5*c)ht+2~MukjMw1a-bLqbsnmR%AB!r&AJrwkedWr;g|^!`TEA8Ifp!~v#INX* z-)?J=wf?Cc2!Lzd&{|T{-vpT4KTN|Q`Al&x?q@5v(&GIRYekEML_Xv~1faso10by2 z@Bhc%djLvSR0;ppxBK;b42T#P#ej|hSU6(5Jx|^5|9wxHbL!la z>r~yUy0^Q9)QGf3!$eD{_|tH0ZS-~n_vnTnxw_DctEMh2=~h)8%81L)mU&>-4C*6A z=KisePna2;slCIZrt5^5=f$Uaa%rE{)s^-U=8lhwnrI)HqTXTf8jTty61Dwi;Oa_y zA9K^T-+23k~DWV|3wF8i~HC#&*2 z|9G83Um2Iw`xo`hbnVD5s%4HrJF11mJOvaI^A*;F)MCEM9XUuNI!2+)T%fOtq~+41 zWjq1OlWsgZCdR#tM?a0ESOtmHG4^;;?{`3z;vER&5d#;n9;$L?`v*swv-0~6^`MN|E5 zDC>6vBRb6=#j>O6jdQs_p#xY6;ZmUHUIl9ISfJ*<1uE|SgN~U%rInm6DkV96z9#P5 zF@ei?`yfq`zbk?788f(y`$r@aC`q;MXI0D*ri0hXrGRw{KBE*m?w#s4$_yqn!}2*5 zspNxR#Cq=->j{n?IJ`k4vZEU`^a8{F$GMNG{-&LKsN8DU3HGRXIxb|93ZtI_mAWS~ zJLCSn!OE?^UVnoP|DpTxMh}1!hjbu8P;?^-FptXjr}bx8Kr?AFs;2Ak=oPk!6a?55Wx5jHO} z$$<(UKSqb7kUZz`-}Nf@>)7{3x8ATalh^u(iG1(}4PA9{OTBk)EAI?v=nZ9{u&yT> zZ&RWq4)p9M-*6^wFm2S(C-{#4gIFP1ODQVAr*{1Ld8 zt7u*eE|mX%<>4W&s}#y440?4W&4T_;N#RwSIh}Mk9T@il4tD-6DqR3=DwSXOG?dzJ zmwZiC;cw-R*%=&%D|f`sdC(nqjY?jpP`*9qS%aL-Ra^$zY(sJ75Ge{v&xmX%anm{`YoexJ;o8TTpaV?$mvAG3OA(tO%A&&T$!KwRd<$ zSC}Q|w^fr%hpiLxgft?$+#hQP?t?pT(qx6qocqlHKcP_S3iLCPv;_KaH5P`=tN(Y9 z<&b^cEEG8Xzoo?mEZ#PD_&n%N4T}o{TKFMTHBMZq&Ik$gS4#4JEevS=CNY9G1F(mm z<@OwIYppZg>4ne zYz%t;NSX)TTS;NwrL~wCL600_+)d;97lmV|ZI#3e`SK~yrqYFhrL>$%>upz|GRC-F zay?H_{nULT$tkaDB1wGMS*8Cby0=L1KE>->m366KPmYXrT;ADf+$6&;gOF zXkN2)k?O~5FtVAw-IJwysOV>A^jwEusG;N08813EWb|BzZ(pPz-?=1xvIq2wV?Wu` z6uvA9FMu|M%VARWu?zG&EU&Wp*DJXC!exF#T2oB$i@p>H*DI8A1N|>0g-Ldb5;yEeSfmpMe;`zNd_OjqTO_D?5;)78zX5R^fyWhgE(jy{tI&F`LYeYF&$}^P zlKlYaN0by2Nb5U^5%h*3Mq10ii5fu(#%FjG(6~DKOG{Lt+Gd%Mc^2cO*v8 z_Y5)8`dDHF{nQX6t;-YR1Kt+geY`@+80eRk6x^ougTx4W`?G_Nz)0(UN)?Tu?;m0e zLgl~o>oKuk7eGG~N%Nq@;*Bp12&ZYOtoW|`xhndNLV4(J{N}T(Zqn;O;WNJ3%Q^Hk z_)ags0nBd_C;3C`cYhtE#fx({4QM?u*6{i-C`v5{#%V3o*;vEtU##dqB@*bFN(!>H zE=`P}-x^}1b>~=H>p|}xi_p7G>rsgj^w1$jS_~N3w1K{Ph>;cpMi?0q9~fe!#W)Z~ z(94DxX;ECl2nr18q28Vs$BGY;)(7KQSBZ_+GP*bj{CMWIQ8^0E+8-d#eE`7JVR(-Rn(iqd0@^taMTzl{;+@P9a2d0OOy#TqAk#N)OSzLz z#!)hZ6WhA?EeR?cDlW8 zkDdnBGO5~A~dy$-bb!fe-ZpHS}0hqSV# zdq>)-i7i?e#x30jHegF**EM5|$u=;9d(n`kG8^(-XiofmnSJREiR^OPvxB71b zFpNpUh>XOAuAr47Ynev*1zb%t>9$@3={92jwDziu2$F{iW03S+KL)a8lFrSw@T}@m zWk9<&yM3+$Wxl&$Q=RI@KYhpLKcwP4r(9M0%?6y;Dres-?5~}n{Zpk+$)^p3+^|xk zGf~0yV@-L%tu(bwR50GuHd}AXIZTXCV9Ex3cS|Qn(>fV6&WI>%Hqmllf0>pC`ye5) z_kBaU`zJ#dmK&Gik>1Yj2Wsv@pypl#DvrEF9SKxgua535g5q&t%w6^KkN=Oq@owKf z&9D*-Es_~5`~`#Y^IMqXyutIG&SlI$W8l z<8Erf)a7stXfA}ieJ%r~sEvh7e;cj?h0nOXhko7SG5eaK!JQX}v}KKQKRpnAU_2O| z1wAB^mi|%EbfUUcs(Xi;d!Irn`I3o!56tEVGMH>&Hmaj?>^b*{^K#)+w+&OT4U=sv7r~s-?2TH%3{vrLx2~MwxuUzlQg+ z6z1t=m?fXJP>^?lTWKr#EN${?a4T&kpQWvGMELp?sqvJ$JOTWTx$EN#7(+Dbl4 zTh${%po|G4(%v-Jfx>6>{q!2H1BK5R3YGhqX7(b5*3U;w6*QL}OCBLV13qP`2{CZSvo7N-0j}{2|Ph6G;ZsSmO-{JY@dkDJt}v)D<>p z&>Mkl6?(wWg81PIWikRi^XK7`+%!PDzX+u8stK*PD^(alxBX?4k=9vC6-LkthZt#H zl^8*<8)Brj%MH;-(A|_2e5Ccr#0dJhAx2tG#z(|WC6h=@gFfh`h2!#=p3Ji?2s4ZaxrLY1c zt@&7H^Pu}S%4}gk>luj=^nf8oT8Ae_&}QK-3}^wxM{4;dt!W{F_OB{F@jR$fsxiT6C)@^9vEr;U19{K$O9v-*Cj?!iaapV`fXwaZI*ob zGTX|d(o3Kev3F~3KeTq3Py1KjaJz60(M*E3MpOJ&`%`>O&wRwgKkohgS1chlX z8iBw}mqehPp=P281m?UX0;Q{~uqXn7=`o4qvs};rft9!(!9ec1+08R&D+fcP=FICpm?8;RXm$eiYHg8-K+R+H z_kpgviAhL}%Hk$>O7?u$=vBs;wLwn$->$j3Qek7uLvD=CjgazU7E<14LP~}LRyho^ z;x(9D^#Sz;D3KPLrnv}|-&iW1&{hSPylnBf@&%86!6!dlW_}9D^*^P~0WIfhZ(*Pw zg^|gXx)BoSyS3lUr&Mi>hVoxMqw-Z=S#_I{wqWW;P47{Svg`l$bB9+qb;?Rk!)C9w zx{}jSCtuLjCeA9(D__?FAc%ZwfnJoXOjFbXI;8w_abraImp5`rtmNX*bL*B4$wn@2 zO&68J-84-U0}^|%F#Ae4%p_me2~0>#vCL^gV%lT^GxFd+v+Ms#8;hllMQr?cG`on6 ztF_0CqiW*#iS}}eWAO{BD8(_MjZVc42RN$Ns&3UKM?iQ<6c9QQL|Cx}0S+Dg?_8XE z_<=cQlXr4F$|$xh&K;{4PgE!;rN(1j?;4!!9p&{lP=t+5Q76j5WLxm(nv+>@4$Wf% z?YNg}fMb|^l+MAxW=jTL>$8xcA^q#iAAN5~8I}j)sATOaSuq}yhvAcJ*cy*~t>pTG z?RFl8n?SB%Ydms}%rr8dQW&s~J4@3yhA9_<^>Zp(zuCYRba$N`YGS@Gsfp|pNK8#+ z$q%fFEc}0^$5=Iy28UNNcY)qQSCKVe?seYIqX@l~hY_fG9D$k#5~xG~3{*ndPF)OC zT01CJVgV(Vz?eJVY#V{dRAU4pR3i|f8i5Fv0D$6&Pzj{rpHQ3rcs$&p@Qj0$x?gaN zvx%CRdtmmUtu!Pk*BYp~T05D3Ng%5{t>*VwRlViTdZoEa8P2(%);dF0V(J5l6Tv*g z@{Oe{GbJH3HOA~-1EOk-*}aMqk*?UQsD+wHDX=fEiOFP+oiMaB>fbC1W^ua5jP(PH;o1;K)-|W1ym+v(G>+-Rw!Exn9X+u2e|5Mtkh{+!Y!uUfSA9PvHNt z%F7md=bUTGU4fvNIl0(m}H-2+;=+d24Up$6)C+sqy4A>~jmt5~Mpx`*qsEHhcvB|25978dt zYa-{foUPWx{5Z8HvYh3(sV3$Jpf!=BOwK23Vtyo96FF<+ys9P+CBirS3xqBH%ACmu zKg*m6vCNqe%bW?Z%$X3&oC&eanGng@A$l%duPJDdNZDPNX%u17$TR#}m`C>{dZJn^ruqDLOmJmx@LQGZ?wy(yrUZMOd#yT|a zq7Hjnk_cGW;UOB#759@>?HHy?rYIjZ|K^rbo4lvBjLcEwJ*{PgNZu*UdN3TW;hc~< zWzPYPwCWM@v3GD%TkE7R@1FW}OK$7`SKY4Vscz+;?D|P)^h%6Q=0%e_&2;kVX^@TR zmeCACA>~6*NUW>rFd^mLC8WHwghW@}U019^0_DvN|Ekx*fbQefKVbxAr4Sft z@#>#2g0fN$j4YOU^-mZf8?h8SpzQly8s)z5RcMOR_z@v!Vwi(jkgI zK7M{;J#Qa9VD!E+XvFBFqtQ7~)cJ8XH^?;xYVuwk)LR~ozfjfsgEQQGi@NJ+?^gWl z4I21^V>rbp9c3le!36HgQ&`Zw)T-@co~zvJ$&Ola)7Bl&HN zq_Ha4j4!Kj6*_>TYsMHG1osp*EoWu#-N@oS8I`xn-~h^m)4cBSZo$-bM?Ivl>U#UC zHK3uZDws@NovKsy-9f4kNE8$a5IRNiZ7me&c|cW9Y9nf={KS!6iu$XG=%d(bVmqay zYC2bGe8SnUFuH}8O@${k1_TPfr!+&M{{XN5uNRdbr3sc5+g<}2AGOM+jhN^3qE zSQoz1c=6+A{(DxEVQg$gG}97^|8MFq5zVxiPKH+@8o%Z9Md|-gw^@5f5=^d{3WY>R zPsbT>!#x#!L?SUi14WQYdTpHci_T(Gf+xVb@bQHyx+dn{)I|IlJ8nYE$7`%9=EZ-| zqWkAq(Tl%P^uHC#OOd+-`Sh<0ZW-Qq4D^MD<3LZtZ!{ajrsIC5=Eg92`zMt#yiz3_ zF(gj|=AXsAvXne+Wl4XaNxnX+8YJ)X6W1#HM1?gmx9i(ys}$05goW&5R8~lq#k554` zn9^FR7*v%*`6uk;G^w-C}%7#*510AQ|Ik+B>9K$%UYQxlxOsOo2=ktka zjJaMLO^IoYrjENxOXtV2z6zf#CZkeYXsV*QYoobEl9=H2dNGYC<8ko~D>DU+Xt_h| zzYU<2Oua^7=37jB$)$w-UhN_)b%f+Xlr(~p-|y1PH&1LWBrErB<^Qb0`m8KD6tM23 zZ2pdxD|7D4YK!Hu%wPpm#YLdORQ0$BG`%l$wD$e+)6w`hH>r{m~F3t?Lpa z=zk3{(z-D*g7yy&ZU-M}ZKG6)8g%<1Mq0N`jG#LWG1A(Q7(s!-^od^x9sLP~flBLY zrHVtKzaCW;!aJI8C7xXIBga8A3xq-KB45BRPG(gV(wVSF%X`rOtNY|@0G^sQOPNT zHdj}*R#$Qw>bkAfm7IpU$`Rx~D+KGN-~bv2blrkUzDn6TDto$G0~)%j9v6Y8uBvyA z>H?Z!=G`*By+sq*?X^^yyXtbM!NkSbpv~MB29n;^n7Cl3&D>Qp(_rF)nKpA*%}j%d z3ufBPT{SZeCN7w1Gk4X@G?=(xrp??{Gt*$=f|)jRR?SQU2=o8oiR`O~=^`Y~T1#_^ zE+z5 zW6Ftq8!+9(JR$PN*IQfi-k)6B^3g92x5l<5-`2K2yx1DsmV8^=t|HUG(Yb|VIt?Lt z#h*T5S{8ZhQ+U$1xxDu=mmbwOxoohKOWU@6-{kUbZTsBjvRh)?l5cCs<$a8~b*pc^Blvvp5ZXbe^u6I!vMQ;=1cB7fi;XB^r)Sxi`M?vg2|-<7JPkgOV3Vx5~#0oxj(MiOFz_ zi~pG1GHzBKm-Cr!q2$cyR(VXmQ`tH0vwHDsk;yQ3>B>ybVTQG$Rif_Os_EKs=Dxc!Q2%0>FFwVW>PA3MC&a{DJWiu zQlsFyjx1lVxsrp0LtbqhEI7xm;aJ^0blpsOw2dUptp3)x?(FvdcH!J0-(fYcaj5fZ zJxOd{%g=C{*FtvEzum%JYl~uWl`)Cd^d^8K{Nv&#Y8Ldscoe+!_llmGNFP*`l*rJ| zGa;lr0Yb{-Hs*wo*)guYD0;i}a6L7Cd^|la=Bv<)tEMiT{jRDylo6M|%jSVucV1s1 z$ebJt`PiAkJv1BlQz&1UUB1QT(>%Gf&+6(*`v`N#M@3DvZ%k3|5Qid4Bx>tDIQhm6 zm0BHX?<2L3Y1?n?y(M|Qqp_50#nyGL*m~2j0VOM?(W!Q9{dR2A?R*OZ^ShdiG?VPV zkCrWmCLOo`Pjw1S4`ufE)!+Q9n64f9MYYT^%nzt;Oiq7@_nD>psTC&d#(aeX#(dRu z!nq4Ip34-<$t&phBWbzxXPL<|`Lm2akBpC^WomhrU)1x5WhNB<7=P(B2rlH7=t?25$H(`8h!EU292;TYtYaO^lxJNzgsC!)6Pbvt%jXo zkD^b#wt=EgJ+y(MCnh7&;nm?)Q|Ir};XhRF(#RQj@_HGssjm^WmPaB#9ug9f{cBi2 zbIMCmBVC6_4_9wxQIctv=I(kiF%>gwr(ha=2Wg?MYOSv1G&)ivnVGlTR zOR>7r;$m*u@Y>hY?ryV#^4wZsU1T(NOm{HQ+co)uQ_X+5e3vLW;s(u< zyGr_Y9MI@T=|#}#CSTK{$Ez;tfaaE~JH=(7%vd|-%DR270<9lKD!ELP&XI|G#h8kW zbR!eHkMT!STci@Z*7;ShFl_9#I=v_8s{p`JXM9z`X*~RQoxN2ZaFn`E9}b{=IArI% zyLtted=s{OPnPkDj)*>2ZJ(%6wu+!~qPe;hb}y%J^h*EmA&Bguqckll!4+xPF|blBv*CO`!oeD#UKVX?Q=i1~owA?XZ%m=hnEYIIh~JP$qAYof8(i zjjpO3@9V6OJJaGt{lvfLmjk|+Lg{zV10rc2^w~-Z@9)w&E-`|>WQZ}DOBh!6kHU-S zJ35gPf53dWi+cGPiLHfXkU#5+0MAh<;eviDlIB7GLrEdrAesKH&!;=@RGteWum6P@ zT4<_Y2cvO?vhq4uQW>ytEH7UPseE5m=-1=m=#Z)dy;9k>aYwkBKi>W*cb5Sq0x`<@3NktA6$`gyIwAl}Shj^oxNqUr_EH$>7BD2{{6X>g4^WGHDwNN9%^C%fvsu$! z$K_n6vXaeGR<%=Bvf0WAZ#IRwZxLqYayCm@x8Ra6OJyaSrA$7ns~(N@tYov4b*E4Q zG(DS|;WBVUKfOp@+mw=4)<(yjrk5E=rcqaM8ECSl73)8%R%Q*kA%b42m%iTQG%$eP!2>sNX7-I}7O1_YIdq6&!m(5IF850S0QG^PH-9u3lCZs%1Ldw%4q&zD^%9A0aJO^PnI>y^q#Q@gtUAmU53!A)E zQx{$mTUB)^BQ8JZ=7CxBu#Xg(GvdfNerE7^%?e7oPKbG4e3~bh_E}wBX&+(k_^7Cf z_T4G!9R`oks8J$O+b@Z&uC(_tH*Nckw;xDe?`U*OD>nI}2h}oGTW=aRpk$>qI@OM? z-;Qm%oo}4J{HF%OTrCgin1mjzT`DAIoaZD`IhPdA*?4n|BwM-+dIX(>8RUh&b3iK+O2c&81MymGR#rpjSvyLc-s%=HQ;`^ihmXReQP zi?*_5?p>fV#T=pIAy8=@9Y@I`D9hNum^aI z1M|NEOk}$^Xy^sJLBQitku`+Z@S^< z?a=+mMz+C;Lpm@I!gl~6P>3UnlD=p{!vb1Jh*33NheuCWZ;e-loSV~&iK&>wv@_bvLy#Eczm-s zZxwF=ICr4R9j?#_tK$w`naTTb^h7@RLxjJdNYdMMZi{at#|+(Xf|C5-&+0kEr3&jh zqw&Rf=&}gjy?Tb{-ahBc3;JxynU{o{>& z-GChhL&)=C3mA5iSpHMSIpwgjtl8YYk5>Ia_F};FB`McyYDXZ?bUR7Hx z5zbMaJ4`LzSNk_CU!JF}qlc*U`+BXez)M}x^Y#KL*uxrqnr9me)Ng(rThwrVoeBb0 zT`U4ulIX616S$JZ)D}zhw^(9&izU`au2x2fF@-jM=hZEp`5uR!kO2EkSGsIQiYqIc)XgyAD~^k{g!;)K6-$*zFted zsg`_U_Qxg}+xnj4uHtircDm{>+jDCg9=&_0QmF>I-tGY}AO5yHF6?OrFi6d{FF!ve zzEYh>X>b>vTWMHoPR2GC%uW^Q{ftSw0pqaj&ygG_e`My zx8vH{8M%s$opsv3`S9Xs^UR)l;Ry<7+>!F~s8CUcJkcaNvGu7;K6v_!v&Q!Hw zl5-x_@KS|#Syo;6fHq%uit9jm)9kEGxo_P*mx01|P75~MYQJR^GLTwDTk;vFs3ntM z3p`|}V$e*0?Am2HVTVZWJcnAjcp zolnD|iPgC&QVg_{TD9csw&d%zv%R?&$?Q2YzBw6>3o3!rz3q(#tX0Hfa1Yz9OKZ;zhMff9<;2`F)R&*oxOH%8fc zP(qPk?F1!`AWQ4kF@$+gLJ?WeW>jNe6qOAD5#e*k?F0HB(a|L$!8_ps#WnBfJ_A~~ zB8;H-QQre2tp~-3ino4!;={z<>;eiLFC}i!E26`T|E}nd6!v8a?H6hULOqEzC;`bJ z0wo&n@=s=rkn2Twk07Vv**hLnUzWI^|eX$UUHa3gXH8Fom5r7d$c?U z`p|~uakK5ZYimVrPN9SidPyWLfnKVl5D%>%M8-wX-$s%-fd-q{esW}90DXBR&4Xes zR0OTBN5%zEYzM|*D*f9Wb<*#3f#S%LD)VPG;QJ*5pf6IAtmQ7)L<2XaG(k^~qVu5d ziKK=1DtcZd%@fpjmE`>Z|50RH0431cj}&3T3`fo=ttS2^cFK=J>nw#*7@(I&(h}%b zl{C80ohvmipz$kF<2-0nSn+X`kG4xN7 zc>(l{NLm6#btpkvXGg{b&}6$9CvsjSZ)9PYs2TyD64lLP;hz&}*!}ugW;NCM<1Bni zjx6E`@rk>+BffpI`nhG29rrGk`BUFeFnMRMy4wHi+;0_% zHqf=#giCTu1ihD%#*G9tcB1$K=-nb|3ACwk)JQ;M6~*U3?-5DsL7N&!-5g4d`=D`` zsBr^mQ{z6y%A~6z(^0g1-@J1ty$F%gg>2km-x4mk*rNR+)g5ZhTvnQt> z2E8DX=0Pu1Qs`k?UrUUj-xy-VSP3zIT2!$B`jJRl0>xmMTWEbfF@iSDkE)wjs2JhO z_Z`d(XuL}=*4e|*#Z6)vi*u+%+TX--Ve4Ut*YyTfj;stdL_e#S%&{zAoj#GIU-+Fp zkz|VefZKa5&OKkDI0*XjNSX)zq>@7M(E4&>{NedQ^$s5hP^OlEq<_7=F;-uYlqH#f z`A2P&N&02eq>{+t*r?v;`pYzcy6#BP=bJ<;uGN+HKIW!J_^ll^GrGf^qgS0WjhK+G z)6V~+LbKR^+lI-xbz0X?O9Kb=KqZa3NA0fHpAkiiiP+cRBlYs*6Wd-ZOV)j-Y`rMC z2)dR_+joPDRQ+d@WzdUPwCtVUS%>H&;{>z-`gkRI z*B1t~77`=qenX5Q73p7Jb-O9Y9#MD^^ih#y%$dl+)OFkZ(O9WC_khfR+!J>gT}^npWq zY3-XBLDvs4(mE_L2CKvW{!D$?DGDEdL1&N)_nfB=OVUCEAvB^vsoaNijn=KXLKV=M z!hJ_`8TG-t+=A`1CGWr1K*LYjD)yAUt+Z^(x3lasJ);g1v!n)}4~{)->=%En{p%uA z5m?9leq|=_!wwVq;0N|?CX$rCs)%=8^nacquty?!@AAJp>E$H3h~&egq>(Y_cAZGF zayay~sJ@s=du^DU+fy|xMNNjIPj181%*yPH?+fRirBG@J^gkkLJ?K?R3U^FezetRr zzZ_zu^@wePF?qQ7xbuP+2Pho*(TBk19F86ljV*v)8A(f^xEAh@w0@QtL2)fG(n^jl zm5vsJWweWPxcK5|c^(uS-tycaHxsC6Zo)-XMK?8iY-6-ye(ncJoa(uwWd^5(aL*lU z%O!CAC&~OmhD|<2sZRK(2tky-eMwm!^hb_U8=k7MC!bY44!YPQi+~h-L;9- z2&RFqYURuASqH|xam<*vpy+@;pqhyfsun>HillkagO%iOii-nUM)^pbfqk`4Yul0ESmUmvX+#Jh7k z0|Lf;pjtlfrgF%Og9sl#-se3gk`$jSY#oq%KDQHl^SMLR!e@#3iOPYz_)IB0M=y=Y z^L`Uc20zZaGQe{ciW$%gBWWJ=V@e9uO6%VeBj}Ywj77+iP?}%B{70kKB~T1{^9uu7 zpG}OQP4o5%))?W-8ormpeuXo!@RTJQlMxZZz!T$3jVjlIsoRFhbuk09$5z#nFGf)# zr~EXu&rm4!1^U5Ang{(zBrSqosH9MGv@VT|^Ppdhq(#s#4ap8W<{z2M|Et`=o;yaS z8+?3!)Zix}tY17t&GmZ4xx9pp@Mn{sg`^PPt(URfUeeNbir#?FOKT0#SFZ^h2HBp1 zo~opfYg%U|M$mT-G17W}Vmw0cm230?gct$6c_b}@9;T#Vm)74!#`U1j9b%+)LSh`3 zgL7gI7Vza(QL;GK_q&PaAJSh_Ej?MNZl1YPThlQio=R(tQoC+;^9+rLMB>usnLKq} zC&ce($qS~Qt-g|rK+SVdD`@)d@t*s%`g!?~_ckPMAmM*rm#Ik(rw9!(wV1a)aLzXMdfF zJ_e5B41zb){E#pC4b8mkg_L)u@k^T>hbsEPVDdJ(oGi{`@jdE{v9FS;YeGrqutb3y zj#X`#oV&9+_~7WMaUQr^8zwS(OdRONFYJX5qE>H;i~LzoCJRYdJ_d~pdUYs6QoeO% zo}Z7+RWz>!SJJ;7$E@?=RF6|;Jrm>``YK1> znE#AduDflRTrq=6=5BBGL=`zNO?}pq(~68l)mG0TsRBdML?i8H1#;_7{vPcOS3!?B)jTQ@{X}?!r zD_dp=EQu0Vo-$f4fOiLi??W z74#e>!5Va@f4R$I?avcqZk53PG%-4Nq-KF^$nrj=0PJ98g4q~%UDtw1z7}p}bG`yM zD%(j4#VdMptfS7Ir1#5LrTZr6>mzBNz!F3bCb6)2b zMe|zIA@CEw8L-n7%KJ;8XDcb3r6ACRCn?9PqVOWtg*b2|YXd&$9#RWf& zHjR4R2a5_UN=1SqZ{)8jiMcH_E?*q4-ftckBurQ_$MNe`L-N>oIqr;=neqg!c6;Y1 zhx%GpD8Yc9JsU1bvOzzuq_A73b=gv61if5IfsxkN6C>!ihZt#nFEN7taEOuC&k`f3 zds^@&n5NZLsssaivmr)WJ0wQX+YB+%+A%SL-f@VL*3O9$^zK89w0293p!XkQr1glz z2>O^IMp}m^M$o?-Vx)CUVgx;Ih>_Nt6C>!kLyWY}PmG|SA7Z3+Sz-jee29_O)rk@G z$3u*?ewr9Te>uc>+QnfQU9T{d3a#HMRZ0bP{pW%}V5IdAN)<-X_YX1B`g~#p{rwOl zt;c>o*b$AOPgYVeO>6JO2ztm6qcKkme-gJ~U?w;y&IAjf+dh>6z65$ZCHZ{@^S~Z+ zk?p&R-jqnkFNCXSDwLCL(Dy3IE8Al+u)Rv5XH)bok+cAMVI(bmLD6oU5|$3qUZ=`M z*Bg~Y^$F>lO8TBcA-!IEhYl3dN0f9;?8_y3jm!&IkyZWdpg|u=Dm!A1j<_Ip#4_lo z$9BYW*%8b4*e7K8P=#_>L*HB<`)2ufiry?{cp3DLN@{i&eI&|2=_7d|8Z>p+GTn8{ z*j>x_RP=s{#0}xQiNw_ZlSJB8d+x!p2bUkNC^Mn>^>igIEA;MzrwUm5G{v#hRF|#E$QqG@UH>%dPx<#8od=Fm*so8$H_y<1gVJYe zxo(6o_*S$b5{sK>zWkI>qNi(d_q`_hYAD)oQ~Gpm zTP{F2C%*vUe7%sog~IIBcfncO*XUX&Mgd_*oztegQ=4aKGW+2xZAKvaY5zvmB#)pl zX@)C4Pf!@T=%T$rhcdQl17n^bF!qyiC`<*Ip#4pq4=_QSDI=PIzyyLK+RP%!G?YDb zUP0pN+Gaw*G_wd(2>i5}QsAe}`~p91<{S8FGx@+zo5=`%+RRMw(`KfEpElDL{Ipr- z!B3m%Ev6a*lO2kf^qAeC(`L3yT>yp24++}bHgSo`k(m*G?xC!=ptIuou&%A3e^T3* zMtT_Nw6DzQw69kB8u<#a%`>N7v9Z5-hQ%6FGd5^5RTBek=56?Cvj~8n_B(azPuYWV zu3lN=PCaFlxOKYx!xDm90ah+k-ft?z3T@UI&}no3K+i38*>`7!&5S@>tJh}gY#kvP zyO&b#qfqBEnh#Mj#`nJbYd`Z?THVr6XwkLs_%lrH8*2w#N@@mWbWlHY7r{%u;*ok@5Q0&CaptWPS zDv&1(>nR6&PBf4LVwUTK>U3F0gwE4Pij9N^^rfaH=CX1=i zByaO*0$7n6la3#VT)s}(Lx?O$uF!>y9k`$Fwnp9$++1D?JU4FCy!ZSH^%&3FOFG9r z>~g(_h4^TFL#a~wpxoT-L^yYIb>{90)69*=k2PI~>$WBHuJ z*nOhW;;6P8nV8)rf)lNApDUAZLM%T4>fF85)}x|tmb%3E$mpB#%;@oY5#P+uf6T7T z6jDebOzVy*M9NzHWvP@UehT{!L)5%0Li^|H0D7e?4pFg}DiqR{ir$n+wz`{2buTE@ z{d{G0{zu2;nY3vjBJvgpyxfp2+B!OV5G(FNf`6(Qeb3##il4w^DR-J z<{P3w&9^Xtnr~DBHQ!hSYQ9Yf)VzZHEyKC|2vGtv`pyX7RtI&n9!9)J%SR>y^Zj(fnaBmr07rY;9gp2x5sZ!hmx zpSiTPBK1RkYiwKcZEa6&rEN>Tt!-D4X`r23|EL0v^HblPvuVIXHL@)7LZwfEmb~{d zmmbwOTVvbzO)lTow$E)Y8zRg-x8t_t+uC*&nFiW6d~V_ROzN9+Hw}0)imxIsRQd#H z$$KAj=}~>NHMV`<<)*eR4(Wo8N34@OT?v zMP8`%3DDmcR$Jc3m|M5{*870Z*CyZnDqHW+Xc6B%HT(0vueN;cXJ&nr@A&+i-8s6S6klunOnDfuX=b4w0-PNa~){+!}J=i1BK71wN7<=T_CKB>&$Wk zUU3m<9HdB8s^MWnOAaf`aPA&$E`lN~%b-HC!^OR#+=iUVbsnJ?6H}q7*Q?4i@F_$j$@Kz#`InC8Opo|s5zk>I zc4QBV4}Ociu%G{qQ+wELC+R&#;&jy@?_MsdEo>XFV|vL~szj1pMDpq5y?yOOlHMk^ zSZ2$Pk~KEAjZLPPJX38Z$wegh9`Eg2CzA9wox8Oz&~KNP?^0g0?wFSEpm!Q#3`+8U z;aQt_WiZjYXHp8f%MfF(F0|ocYa#R3_>IOMNvSaAO2a1lbxw@`H)u&aZ)y>tv>P4VYxz#imft5RRDf&=M(FV|uE2(nFHqF&|w~bN1Mw3;98ryoK&f(gn$=ljS z`i2XvoxhP0Y``%sgmaWlAONT&0k5 zg+_9Mx}K`rQK8&YU#uR!RiTiu@Sa3!RC$fc8eLVO(kKKOgex-z&D)fYy=o%I!q=E= zU#ffEMC!bEbm(rXR3iObRmR=c2tLGx%nL|FeFx$g6gA<##DN+m7pOr``q%G{(7H#` zDtBN*tFi3!YqG78_dD-gUKWnI@eymu`>V%z-d+-kC#RTYfnem!ScChVZH>IoSuQWD z_}sX$uopr1DD@c6+e*SeTsvN4^RII4___t3OgmQV`O+?flQkkDtBH7hk_J)}bH8h1(477qWlLu9+>(%T zJ3`8>7}I~~oqy42zM`;U;Z3oZqJ>SdOUm7R?R~CpC^;w`N)9T`Jb0ZSD?D5z2d|4B z8oNbY`EZP^c=X$G=j+8WOx}l2P3VJjpPNY1pQFlmIjijT3MEDI`_g5l$y@r`FARp7 z)2qn;zLBYvTRRFrFglrD%MT`!^xypC904$7q{yfRtP6=sV7M4H>y_Q0jwEG^C_8(6 z2u?@rmPxY3xr0jY73Z@J%xBMzb~oUBnyEH0Ej=ghSc`SLkIVm+HkL{oOQnsa(uO%e zcURvZs?cwyFz^Ho!rEYUqGxE3!U-;?e>-khRXdiceE5({J-t4r6>=LB4Xm z{Ca_UhN*SKc3diXc<9vj|=Ic$& zXYDeBbu2c*KHUsksD#~!MyNyq^eNpEt;hi_dGCMT6;@1Dk2ruvsWm?!0~?<+35n+o znH#j?1C`d@;*j4T^gc=ojI@{><<&qw+i}wnBOwEWXSq*OxVsb|rK^d#)|!Yi;B=~S zm#hBEh8sM~ecH-!J8q+nU_NzZW<|c=AY%);AZx%k8&dk;I&^CyN`a=*sj62^6-uQenGH}WBoqOyxyWbl zz3Lh+is%*USVJ3m28P4tZ5&d37|!PxoJ{(qly<{|6$%Nz$fDIV-rB#ae+_LrmfG+O z=*KM*zfldc?Uipg!v{`CjSu)nAk^ zI`k&yvueOkz1X{dd=qnjKArbaeIsDRyW>w%iM^xbEa+?`6-)JwLk=dH5k@?Hf)lm! zf-oq;#zv}YX~9Q7sfA{(;L6K1=n&6)^<0V18wM&b;PSF)pz^{VAD9bNUITn$TEhij^yr%(>vez6e;KBAec ziMe#;@E#pT1;~-3Djvh+HPdCAm0fi4*7TWim=`4+RxHTy5ndT3^!4=`fsp7@-sLsM zQaF2CHFaTnF4e6sy<1;;R~#XR4#+>iEO&~@)X9y$c95){pDN&rjl94OlnS_FWB&Qu zn#eoNth#GrUMoGy|eBPw9 z^vzs%hnt7e?62L$9Coiz*3Am*g$7d*pDH$2A5|AwpvmScv;l?9{>YzmU(>F?Ar8Lz zJL;g@H4eUc-fibF-P$qL9iOYK9n2Nh#9XBrO8-xx)a(3~hWoP^_99;1Erx9_Ev}wO z(m&0SlSP%?CZ4xSzc5cxs&!{EQ}N;Knpo>-{Nv}WT&1j$Q21YQ0yG2pW9CddNe(N6 z8~4{ho~2Mm9Lq`8FG9*csv#sk*TBpALgK>*Ka2O&4WKtf(jq9IWDa-fMp81g^l+*4 zaH;fgsq}CO53z0B^t|;aYcl2(`twd~Zlig(EwqJyq=(CND&n922`{ zJp+1=SmA~G`t3QD;Hj0=-a`f#>XM(mU`@=Os~l|KR`wBW&hgNWF9(wu1@t}fM8*HK zMQ%fWO;@kOXG}(&HcVAJrr-d}aPC#%hQM^qljuOrzv>}S^DlG=)I7X!?{F@+=kJZV zHBZZJd0KAC({ek${a7tVVv-pE6MsGWH4HEL)QKeh+%Z3qq;q_y!!;Ls`~DM}`v=7Q zci%8g?WRz^z7X_F%i)s9gML{_GxDbK3H_M^?l6Gzz=I?6q572qM|>d|9Bsd_J%?@7 z$vWxWjk&^_Xa?g>Tp!%{s=_tys_l8x?o|hbYadkDy!O^-hHLLv*t}+2a_am}3M+Sv z1IK%*+p{+IbN2%^*BWkno_ZhEc(6j@ziQ*)x`Tp;Hz*9$XI&cfJXc|$(mGP9Qmvpb z7-F=Y$Q_Ue8oLzsYa-^+S`%}ny%^CGh9X*-&s68!{Z-8)#;Pm%W-xW! zW7N_Zrl1*yM&Dg)_MK4k}w$i6)RQshou8Om{Ub*?_ z)hnbtd~=C_Yvj{llIcbLAH&4GA|(Rmw>>?KE6KiWTTXmNXuV5epuSbd+=tWH6W*_C zi`W7$dQ{(RjcwmIxqMsOKDW7Sh%oouj@y!NYui<18fe?_xrO7*)Hf$?ibEV< zMP8^@-{$h(MyT z{kE&P2$cJKnLRGvb7*-|^2sluE644k%|%dzjW?gG4j0#{ob0O3Dc(2k6>R1~3p(~n z-WvC?j?4KBWmN|y3(2w54aageBS_iIs*dL*$BYTQBYU>$FlxcifJdr5?6#Bi9wYIO$>Vc2o~+ieZg#zwK2fER7Q4v4ZoJ1Yok-H# z#1;#48AGhfv26`2k$j!nOp=R8o;2RuH%=t!y_EwVU6U`RwYiiSt^Lv>9`qSQj6q5M zFFXe&UKvod4ogZwpEbmos|#&-*kH&kCVK-~>@lQ|LC0@5Do3I}py?ZN((^vOe12q` z1w~RS@Wf(_`^ukG)^GSGl}*|EsCxX6{ld0TUck7>4W6sTL~#WwTgE9pdJ5Fv=_-Z$ zR-kfT@{)KOr`j~n%Xp=HNRK)chZ8B1K(3N_K$FPEsDSIUg$hM zPb_q@d#xtie`oK;{;3yi?BBI0cyIm6(S3_kbqA@Z3DorgOQQ@>tDD{$c7_5o70yLIAwMtQIC^b`$svFY425oflDaB33c75e~#~yH8UHf^wzq;wERsI>$ z6YS)jAk$UeuRk4U^ z=F2GdMa|ekvYcs`l*-K%j zE!5#*1BEpam3yg5GZEsoxPGQO@|Nh(Ea;~rX(?A|JZx$zL$bXyK8a**uarU1Cqz<_ z8-JIYOwu0^PbTSlIP{$H1F#ia*TMFTaoT#3ZEhT!tGEmt(fb%18-jDN{hb<>(IPK6 zNa}|Es>nB1U1CF>vA*%kRjeMSR>Nx*{k5F|l%!x`$P;OFqxe{75?j($+zfqrZIv^w zhZ8`vKdN?Y-F9rQ%h2Cv>=e-DGSKv`(~hlb$0i>x)J{DnmDDBW8UMBgpLL?$-xh5^ z;|OeM$@ib&3n^15{Hf9v(PkL9ZhK4X-0+-a%= zr7|*&6WprclV3y2J(I5(P-P!hC@)7CA7b@WX8gNIS)Z)8@cNw;(sY;o!+o(P?yHT{ zn#A8~W8DD_&U@)wMtdl%#gEg*<|c7bkB^?Iu;z@uDN>DfSP~|$59J;QYVLBN<~|2% z?sTB$UI%LKcA(~dhc6_6`hP&8$2%N?{GXU;ztbVT-_v>y-ckUEYnj>|K9brd^_u6y*P%+`|#C?eDK5eTN6q8o2vgK=3D3t zA~}}Vtf-S4b;b=;hTfD;7lFzkd#O$hff}@@e>ot{?MbJsE6mlz+*(b{jn%|tC1JnQ zto%Wtd^y58qPkY2^0Xuoux`u<8jOW@$Nfyzj$x|g%?;B zP$^jE>OdvyOxA&FPki(R({ij0j_SSoOFL6h31`*KNtyn??^%^cgQ7^jq>xw~S+( zmV9d@L3p-Hd^G51CTj4pWADA&HRN|p@=`<(RJn${QCc@MtRgLSrZA)ge*okgNBs_fr{8s?3dSa0dBrM$5CHs1(b&LcJ(c<;|w)NY!8o#E_Q^z(8l(wUggu8^B_ z0dv~}Loarn{62W2qiU4>NundhYE1fic6&cYoEtn*_a4tt*u3@~rQJnODVomAjvj z!;-7av}p8M44I6E(0hCBB$*UJZ=ng5TUVZUA(8H%Ca#`liyjcJJn=%x^DU%2%|gnv zE2KQBLdtU~q!^hZA>J`z?i3^6aGVA}nHcIqy{?+NaA>xw>QF{pesJc2*#lOdooX(2 zjYIZ?nZfflD=6v8H~kki=+iv8w9o45O8W?N$45m?v=2>D?=U!Aqeh8Dy@MI`>PmYb zbJMopc>BoY^^S&vt=PJ*62Dqx}9&$8_6b3$yEwv!%)s(OUz(D ziZj?!IfE^g)74Tr5fy9kO5LGz@JMEqF_Yc9^;XhuW^2xL&(KJp8zY_teMuxOm!_AQ zXOrn=Ce?eyRqQM%_pA|brsEOO{^jzICuMj`>olz0Q+S~Bk+gJxqAB8{kAsceg51uJ zd7~~{F@@gdwcJ4S8r}otcf7R|UllvkaEG2wOv`8hc3calsvQ$%&WJu0=6Wt?GS|B$ zn^9m>?R1rFhOT})T_u~Li`05ZDlUsDiA$%$;w-a)S!PCWF*4k5rf6g7R8BnB3aDv4 z$8&G$+%pm4Pfg4{s)@NzH8J<9Cgu}jP0YPX?ejD{{~oinj8`n|{VlJYpO^9TuVUtw zLCKtTv#esrVA}MsTd*13-Errr+vqZwh!vKLRYZPr$r0-QS`eN_s&Aojc^ZN3)S#gk z*l}T|`Kan|+F4Znt6>Lw)HOXrO{L1 zL@OPg$2y`;^FV!nT7QOxJaI;KbsZixZm^|#E?%o48Kc^tQTUOT zA^XIUyNA`4mSA(sM*P0-C%0=!x3lZB^#|=1vL%n6@%Sh4UQxUx<=ih+?swz5^;au1 zc^@h@v}Hc{IcItzN$0p#?Ako8^sbbDEJ@#|tKxdELF4?mR9*!AK$9<5S9MVbG`B9@ zDJ}zL=-M<{6t10D<}ytNv5%|spGrZ@3}{@Of(89tlP|X{zcQkizZ4~lgAsWj zLN9WHWgd6rOyiHMa_&|ttuw7&8}Gu|&Y$D!mZto(=K2q7-`)^zz5Wismpc7iPdq)1 zNAjQ7f#PYW>8+eal&9TV2Lc1b)35~$r$ku(Q+};fKYn@DEA#=@_Kh#E>aQ&~`_ob4 zq?QshD@g2I#4o?rs=C{HRc*0E=uPKNRZB-`|JG}ibG30nlSr>$sl8rv@*Q4uySc;( zwy;Eh=9xx6Ulo>L$JG0^!t(1>53uTD5x9~>cMY7tl_aLNSfanh64P5Ov1U8`2d)%{ zf8d>J;g?zyb!XBoZ{Ff=ry~M=(4;ElKpynFk@Hy3NfYm;{l}O4 zav}FnROGcHB+7gsdUi7pIa{|68_=x%UOTp_c5MB2z6D=#pM)B~Gw3HK4uaA}0|RXISle1(y~RJD zH7tEsGvwD4L_qIa>x}+3OlrWD=E<2AB+7gFHrt^axRQ-+w^*Xr@AVnvz`B|-2{cRG zRaJL;nf83CZ_pe5Aqv;Hqum4UcBFjNeDe%6;C5VFJ0n*y@ov3M+N-b z8U2Xg<{2oL>6JC%O05>!Hwr0=8n~U?&Jp~M>b`u{4SunzuG`mE-X2Vtz6`Kxmm zGibPwCv~Py-0_lBQV9&{mMaoFwwsRUIqbXR>yJt18Jr1p(URv639>c}_tfB@*i1t1 zew~Ee6Eibjc`luT&sB_b&DP@UveaUKay0yi&ngG#e;3Gh&G&BUTIj9eocqs8j?%Iv z0gBd+zfFYA)6v?48)Hwd-#kO(^^v%=d4|TDlsM&%_2Zc)TY;_G>%KDtbNwk>_;$4BNPnQo@!$S@nsWw*07Pc8n6h@tUh7zSw6B$VqM@`JD!|XafulMJ$6`n5pf3fzG zW;fCxT&bWkSn2LO*byUvEHZof3%EBMUMDWK$Gs!SGD@}m@M z%Qq^WdyulgD&LFR49DXg*_vOVh5zlLX_nIso}!PQIzO4o%iNFf*VD=?x9r=&usV zX>>P>QO^HHqr6>=a(*X8AC^duRCFnkQnZQ<<$`d5{uwm*y<-ZNNx=)1MbNy?$ZLZLvA%7EE1tZ25DFmCGG7rmc?KSqWY`VIx&pXQkOQ>dc5Oas~5*GdB!(Q=!1OTZeq#zs^k|H z%II7-AX{%r-B7;k;yo^<<##trY2!yyz4V2dZCOSj`?@&njGSZjLn<$|=+!m!?;=eO&!5nc7Tg5IS0VQN7}3n*nFtFt_V z{)Y`TeKT==ynhyAJYm~<@ACq%`)W!Zz#f6DW=!716SCeA{Um7jxauxub@{p)m3vIg zb&)dpjoy`+ybn*E$Om7|^Akz>AxuFW7yaMB3ZD3lk?H5hTr$*pE^Lng->X|AFXHZ* z>T&XX5#x6o*JI8dF_C2DsMsoQ9E!J|ePUc>yh%5gAqliPQG5{;jdr#=?x(t?XR9WB z#@7i|7T+o5Jk@Q<*PEEnZzbzYH2&#Mcc$zm|Hb+y_-9obdC23@UHL+o` zMmtlTsk^8Vpcpw)NkGGHcdA=R)+Uevns%Lgh?;n!LSrC#54p@?fUuS4AVsD5!8@6x zzfF{qi`cv@&IHDECsU0Ele}#s+39k20W^2YTm%}O!d4ZQ19hxRAM7%q@xS5(Fbn$Q zCf{du3HJTC@H949Sn_>@wGB~=EPiRcHd>nlP1b}DYga^TLZtEUaWyyxn!FM|Z@v6G zTul<4Y$iaHX^9EbNu?2JQjc1~ki95qQoGs_h2=G zM3zpH1wzVOz&Z}AFNhUk?D3A$i({C)zD*PQAfINKSn@QLJWpZ0ucn5+l*+f9-Mz=9 zv}~bE=@I+tL1tSLjO-KQ6;?yKLR7soL92;MfK56+!4k))r*$@S0xBiB@& zdHm@2)kuF)?bnR0q}QYoY*=g59_bk4oxXC{*DIV?4zwTkI-Re!BQW3_yErL>9vcAlFR7iOmg+%UvLLzZM zA?4X)$DVw_aOm~icA)0w0~O1_K*e}t$emJFb@C&5LMmiB0mV6qB=<~6xm!ZQ7od=G zhlG@8#kcR9jV|#5lb=Z4RZ8xx7k74FReS+$6tNMm zSNSI0ke#nk%%8X^K5xB^-UI{H^Edx|#~Jc6T((!ahP+UlvL5$0t*w#wItH2l5B4H@2_Bwon_t)Hh318pcZ5{7{jzZS4}D3qWvi4pW0LyTw!Rt_64k9^0-tDszO!nY|0E;PJ(Zxps(YU#RMwC6?{2?qDvCfvq1 zD3rHQKu?IICD0cuDZGV3>*U0Ezq0LeFs0%uS8?}Y7{9c_ zy^a$y`;sw?_oC;;rrS%tW@1TVJvYUCZf8KkqIq|eP`#!6?@cO)y!edpf9WM5n@3L=>tLSMuo;SG|KJ*c2VbzEDUmuff|%p{}RyIF`zlnnds*d zu5Xt}w@~!fiF7+f?~q8A$;;H#4_D8M#g^{8FJD;$IM^ zoJJJv^Z$Hq{Z`$fB+L`%Kdj6Yf}kv(oytQd2U^dK^W-Av{~BU!WFn#$eLtWVE0j4K z^b1M~6LgelASYfC8mB6h1w7~*lw?n_ezYR|nkc*odRinEDT4rc(Gy-f%8-<=WVnHV#K}$@#+0h$pYvVk+cMgBQlqQ;)VBVVLzX(uz8K10?H4;X2DpWg0r}+*COP3j9+EPKN3_uj>?|}{dpuUO>6AC z#Tr<8tfHh$rp`Q5LLxChA(5ABH7`N}{b?jE<{1*M=(8drjxhmrQvil*xwHejb>0C-9LXb4$&MyyY`9cz4!^H8IbNPxIu`KC7!M?IX+`9~Cvx-X%r7 z!{9898YL2Sd1vuaudcNBF*j}djkotoUYB2MT)9?k@-+#nWvsT|G;Bc0N@;Ye9b3O0 z+jKkMd%2(6jhF!tv(s2rX>tAc^gwx z=7)Qfy1$@M&U-))kEG?&qh;nzpa?R5o*X@z1x-#aNDx-2mjpOWviVXE3OVP<3WsW8`bIn$^&OmaTs*E^MwHfzL=A^Mp>K2^P-~b6X`%jQ*|$u)&2ibcP0RK6m|Zu*Zt;52oSFY6qYQY;vGZ< zZv;GGQ729&qD~Uz5CoBMqas8_MN#BZ@tmNjcp!+n>WXVT@W6wJpyCPy1i=H1cz`GW zs#o7nb-$|WnVy#c_diO$RlolJ&Z@4y-s_i~YE+J0D|YT{%{dbOy)+KNOXDcKlt8xB z4&bHqwyt&yn#^ra><_Z)ya;Vh`Vtt^h;f_O`&>a{N$r4 z6JBP3JLWV6iLnfc2%uabmBJinNbCV2;^Pd(epyXMW+&EU*admJ)ca+>?&p5en+req zV=gmJzK%>MhRRDBET)&2GSA64KN9{5pfLB8ztSe?|HI>_Ya>RRtnGluDc5Hw30qp6 ztsDQ`U&cdRzRWBu4H11u(PmCC?5EhZu_7glbEgfdXhU+@;plgA+&gDya&Udtcp^Wy z+O}-5=WHc6CTcfx;;7sCb=6w0Dp4)>|+0-bM$v%3kJ->UPxiR#~?#2lEt<|v2Q z4B8!i%gT&zmBVZWEo;%ttgK6~_5%ffb(O^E6rF3TWNH4i&WkDAR4NP&h z6u_Aj%wu)S_8Gz%6`XU(drQeKtTbO9M9a@q^D~tw9@e_CN^@nZnYlKr!a&oWpqg0K z$?|_BfwmoVl;0U5UiJw!FRDas%}rwN-9dK}bC*=2*j%SPSR292A*pCXG9|ek7u+T1 zET&{5m~q!>Ln_*kOgW`kJ~CZQYy`70u@TIWijs|BhU7-zdD<5b6Z&H#4}o6aCrw5y zuYd8$QyG0LjrH9Qo$=pV-`RV96u;diaej(6Wlw*MIcPvl9`^iDxZuW!_3xDdn%f7_ zIL!wK(FDyMC7Qua@V8i>-+M;*sOSXE^MYuc=0_y**6Dq$BGSC5k4W!x6_FUWEu%>yE{LU5^e(B0H0#kWouUWYWj#*7 zisI@j$6;8-i|HXvS60dS`kfR_mxULxk0L1cWrlN6MLC1{+xblVK#Bb7Tt2RF%kLY zEVi&Q5xJ}0*xqbvyW4~Mg9H3;yBH^#dL3o zi?mo@n%SfJ@(j(xgJ_)QQ4-lNec7+X(mOgx-I`4A6+wN1=7~WxPV+T=y6evK@vRr; zt5OUTHJCZ8=q9An9J_0>I{1Bwsys>2Py%b0CMB?+g`r9Q(^X@RQ&*r*bBmfW_ZQkQ zX&$(T-(+9(zy72{*7j)T`(N?c6Z&S0e`WmP!O2pZNAB*O*wa{=S4!kYZ0QueLeW?v z%}pipM0!grBF)J@B0ci67HNXsB02wYtxwTQ={;VRuSx!H`uo(mz{2*-dUmTA+%TiFaJ{47{+2t z6&`t962I7cDK=go*&<;k0=D8=fSgP~BX6F3geisHzuh;TqFies{(I3O#UO*FRVx1JQo(-X!K^@PL%_Jna%Jz*SIPZ&ow zI*;mrS=zi!*bHW}++*Y9@s~q4FhYJ?+()(w4a-1vSY`JzA97S5ZV!A`3PZwrBlH70ozB+7jwDWg{I3BSGcqvCbPCdLdKIQOIjyMljAYMvu zaR_gMCg&NR*dL@Fih@8Np@7@+av$a^Ogn2SFK4Z6X)WdDteJug!F;CnXGsi)75> zjmiGi*qnOu)FSOSJBBN&Gp)2w*>>E)XRh^Kj~pQ};*7mF#ZJtxtztj-{yCZA| z^W!iwpY5RKW#(F^^I=u$Gs4{Qyz}vdl-yqH7%Y6Fs(t2lZ7I**olJ(JgOMv_%$`MU zNKTKt2`%%3fgOX58>+xEOKQr?7%Ms$`BfEIdiL2(Xqhj>bqqHCUIms}t5aUan5!UW zHmP4y+^!BMTG`W4cPAm%w)e8o{kv>r?(Vt8uA|NPZKvrXMq}G5;teO=s1DwGNBK_RHrqpRG?N+O%m-NaSz+(&KyEmdL}3 zm0$G^tcd0j5_w&EM^!|cujnHZ=yaGSv4kX^7m`>`ldxSNOQz@@T@h)%s+R4NDSB15 z%gJ`Lki>GDPpKubWQyKC6_I8=+w_GCKI`{raHhf_jnVu`5KXRBxT``Ylg&k=WUHlL zm}3p?=g5Hj0t0QzRG3wUHkEKZNXi&nx1FtuHjOP)Py{e@Iq2r@ zziVgNrP}M*KiDBN*K1C5`$TgB&B~S;JXp)mFU&pY1tqEh7URe)D%HFfE&hhl|Fq7@C1Th{gklARvL+FB8nkldb zT3Y&oi=s;eVCE#kJ#aDR=CAmo{ZeQjrqG-hD!$?wYED+7daZNqML+e@orU%gPxHA! zw2bC*iTq)j-Z>SK=DYfc^e(Q5G{5`5-oLlF*Bak@snAQktoqlTx5p;^!j09>R*ot) z-zqjv8NYbQ&oQ=MrZ}=|WoZVa<$RhFUtr$nFq^^5$~A0D#OAB340fBkM1%Epp>?ra zatIimb8VMz>^ffqq6M=7x_NNB(khgRy*YJ+n#Wfn$Bh5(NHizVJSJ!+3$vTDe;-KG z`6tSI2M3b}sEO}}!^ntemF5JRFAK)9(JmNBvncTdX69GN++W6@9b!n1H91n|z<{M4 zn=KW=9i-gkDGXb8lBwKTswrTBnf#1-v`p+0qDn$H<_X$Z6(KVad(2F7GXxteDu-vQ z`LRmmnDsw*JV$eRpiP{cXeNOT=k)5Pefu<7B9_eYtuU=fnd4OIjswyW+SB^J36}e9 z!M#O%j?f-fW2+qliGyG!iHbChrJ{|ciZqR-jyXmFpAZ61CRoS3S{qLbq+~BE1|#Km zpnBmJUGIC36WV(>G#9S-o9v29^IkzTM)RN>0*mHLf@qB9D{l-e-)Q?U@jW5Lx0L30 zgJ>B|g7Rl0^q#gJ=gVlGBatW4+baaVj3xm$;8dMMB` znkyvo7U>;Y5ouCfPmC*D8fv(w3T8ijv-J(`F?sao78$q{*`8(6L_NEX?kw9+oWVz7 zFtdNfd`K>ul$m|~fHBXKp|KeZrp)Z27&F9|A=y1bzNR6$&p`D4Z(oc7?OdtZ5;Es5 zTJJ}}ifCR{iMG{b*|VBQOsxwm5p(09YHoO=)xTXEMYmH$^6E5__f>9*qwYxLeAJH1&%t_0Q- zk}dV`2?>u8=n3P3_k=dq{y#hV{Jlobs|hf1-g>{05bVnPC5Z^YigwT_?-53Of(h`6 zN<=&dh6#`qgh-4zJ&on^)bV5I%CW1$C*_iLniYuEucNq;SHb#LCu76zgt596#hR}F zWqJRa&>l(+s&^_)C(HjmW4)IjCA3cB721m8wYVXal`?XG(7DChn2L}Kr7(nnh0OkE zPj_T?8d6af<~~Xqv}Nxt!MiB}B0_GWztY6a%p!@-b*lQWg_in7`TwENimq3aaj_yM z#eEf3)<^F~SAp#f^1(bJL3U!~IL0e-T!F~RrH-6RJbk^F+A~s`&j_Mrbq&|<*#8F8 zEH*;3FwEXpuD53FpPfGMRmoXT@~4U@u;ymwof`v|EGkarw$k7+VUYKNsQu%zi*49_ zC{}OSt?pRwP5n*ib|F-ER0R3mDn6sL@nz3OIwodjLs_kwa#mZ6>rLnje?Q7lPik6_MtW|M52R*9q11U<$lQEGA~! zITZ(7HaLlS#=JqX;x=>dB}Ym;I?L~ot&fKMC%&fUHI*oh0pI9n66%<%vSI&k>+XT%sxA??~iInt4fHyN7wDfYjV%orCO@k z!+XNrwX{4_*b~zEPYLb*Op~P`8$MCRG1-E|5m`|jj};LOUb2-mW=rMz&~Ox*!2aJW z5z*A~0Rw5eL)fB$H0^zA={-}}P&kg4|E{Cxv_DV)`4Ww2{pSK0U9i55_564pV(Q!8 zC>e*L`nGxnUAr_yK)AWLzU5fy6IyG|UhCrld)uiDc9o@h-*E7qe7KrpmFN&PDTzHb zqXM86#RagUIPc6kosVvs(6$fJPI9VHi-X+2-j$D;Gjsla;GCMn7Mp=H@W6k+r=2W z&c{u(`&3bHnVfDErWLJhN9&GY4YXBBoes654Y#9>w2RG6q0>}~H;1KV#Z$F1?iEhV zS6E`-U-iQRloC63Il&YcFjmBp1X>YG6W*+-#nRLlg*jNkzd~qNPnvHFqUC8k%ZUTD zio|p8ko6eNt%GPeA)Z-@`a|+Ps)1|!1!?k}rm9{SZ;OJ9F`C%0SM&tH;VXNQZw$uJ zv`4w{%K)*7`nEW!p)gV2848=s3S+2xMOq$P>a-yh?MS#eKg5){>zbI9Jt0YHnSu_r zvz5{^wual;N@*EeRf#FYRiPl^+~KKV2cP8J;boQRNHwboJIR!(CT#Lq(8{rE#m;+$ z=F|oEJz*R`PZ)>L6B5Lp+8KI6I?oDw$OKJ-?eY4cpqRqb4<7REFNXd%ZF)c_%8!HTe>$%moWs5y$E4eXIyO|S5-OjJ8R#&?M)p9p7BzmqebEDswKWSL+ z3S*xc*1yh7axu&sC~%mn#AeX$=9!oMvlXw$W?g#nas|JCmBi>2otH>x=MK$->%7>rF>F2} zXl70x*vRb}dkGL*Dosq9{yt62Ze=oeGpCz4&X{u1o~HX;JU_Cxa%sL(o0=YD#JucX z;1(o%wk%J0%vdt!BO-lLn7N~GKLDYPJow7qkxWV1yW__EUc-N9mMybMczkfpVHD?^KX8Wovbt;vV|A< zJ%-*+lG*{H`K>L3F3q1xzCMxlf_<~EOz5$-fH!)y<;4CA}c#hXyV+yuQK*+ zMX_ruialpubm^WnkQ{zWvuevJIwz^nr8zlsOE<4u<>gz$IohJ62=*bmoK5T0K9X-k z^ZV-?MSY$I&VJ#|zLn%{>YdXkrKr!>tnu79nY`Uzd1A9YVQe*9$f6`rFiR`vvl-0N zhT74F+tEha(dI88CNLY@0$WcA-`3#xLQ|-%rY(PcINmJ%w+n~t7wLC?&=lQINNjf@ zns^*>Z!{aWT%7(T9BjvE&Q~0Pi+}(#Czr)2F@gC>R1UKp%=5Y}c|$FE!!6?*wPhwU zYBrlua{ByXhhh2GkJ5Y_8c}^dI|mu+{HQp6|A&?Kx#2I&s@cpy+=6Z`dXEyoaQ`q( zp9pra!ERF9V=y~s@j9RPq(6L-Nd3@Fb#jMfdqma(04uY<56>H($NmBKmOa)8FuN9x z&Ku2XFPcINwA$(6a^8Y@3-AfH_r1x2O*TTiJKP<{C+H_0A%ponXfyk`peTF)+UbCW z4PjqAdCRj$pQ3~FGllcbF=po-jx^g%ojr;ST)?*NN9|U8?--6G&fae7?Ij#FN800s zvxel^47lIYvxSV4f=Y{>2Xsfa1tFI!m5aS8zvg5*Bxvt;662%gGdBen)a>oucdCLm zQ8iXMTW7*)iY>1EXs^1jwzZ<(pdS;v#ny_L>KVl~AGV(bcQubIKI-MxN?dO=sB%9p zQdJ(-SDiS1U!6FXe9fHwSij0YwlQ6Fc`I)IrdIq+?9a_TQL|s+GIO7!vzm|2jd2>C zAG0{RW-GJHvFLJgzD%RoTg5mx2eT_^Y08bzHXU~Wb(cVRJ}yKse)cGxS4ueKE=|uK zrOVlVX)Kp0!6Eme!m~%YD7&|=I(yu>kjA`L9^N9%JisX0kc`3qhh!-~$%_&mnCF#4 zYzKQwMw~!7%x2KiE;xB+THfn$%Pd9*=4BpfrN;aj&#n*1T&EgCeYqz0nXnumszp7Y zpRY@7!N!F8fl{~AfF>3*k-Iq@8=Fly{(?5PFmO0U??%R)nKMI_7|a*7Xvr&E^13Z~ zelWSy^38DL)ZJ#F_nmNqX@cf=`-t(LQj{13^GTOOYzG_mPd0-M`zM>hyw?%KP;kr5 zuFdI9yU}(u`<6JZRor9E+@>npkU9nTSV8**vQPN=*k57y0Zll|bw{^rUTwdy5#N9H z(td4sV0M34xGt>P;f^jw^LKP(b8-LC3h8M=d*MN|x|zKqy_vm&=x?d+X3*p^(|+WE z8_FD{$4TTci5)L=6h;cqCFZ00*zYJwkZj?l3d0DD zSM%l8G}V-sNwp}MA7Il&?U+O5?&RRseXl6ZHs)n+wwGOufK6^I^N-aNo5zPIH)Ay4 z5hmG6j!w)hy9aZhy_!)gxzAobM_O4q@-6PSk`y=V<8-1Y=q`E!xri4rhOxT?uTi zq4UR(=c>1~PfA%Y?K@xksd~(&{86aMU$1bnyPh!i))U6gvUM*?vI4WTaz2~EEN!SA zZMYq6q#bSk0%8KQvALTV)fk?tvGgNA{f-@AwCYoIFIinG)D2;}he>SrM4H4AZbHVP zSS|zk^KkM!Mw5vY)a%i>b}>p!V1CZoj~gjCFwg6@Pl!*@-Ofh~Z13CM&PNMuukCK&m$%|>b*EOQyPZGx zIl3G1`RLs3{4~1e+xdAbv&*rt?|I|g|2~@Ky_JYlf3PRy%zv%+o1U2 z_tcm_n`(GeKz+HL;4@+DId{qD=l6uzf{h9Ft2Bk|G@yyaOyuqaGd4DxaQroQY+>MV z{H@tJGiQb>YBbM0Q9b{9u?5Mu8-)ARR0zw2ElyNGqo6HX~wV_Yni*8MH`YaSl}*t_6r#?DR)+v zGnIVy+L#Y2G5p!@DW$)xQv}+lLLFMsGIi*-vz5{^wv73&DE!RDQ^$N+q^}7xB>&{Z znD6U$_Fsjz{CvK>dMq)?&12BA(#I0X7pj_l%-vTEoR2hzHs2;_j&6Bcf#qZl+O;;V zK=LVmmWLllRqjo?*h^1{eXu8tJ!R)?lP?=-?8%C-54OvPd%gEWK25&nG9Tsu`TR!X zba#wTVes;6dC9$e(|(UFnk$)|w`2Z1Gs(>d_UbKk9cOFDx_5P08$=?+*DD{R+HW1zr!&*Ny(Y_f9n_eiFK#Yeo2hx<@ffR>^GTc{!k+S z1uS|G)n^JVagBTuL&LYA%6oa!BSJf;6TrNkKH$)db2P{(L~H^e-dMmL#8mn_ni zuedVq4VRSWO1UY_$%Z!m+|TCGrZSEfJ9o19uVbjp;mXawuQBD>t4}c~T&;#E+rf-A z?KP|PHlNmmG1!Hd-SJ)^BL`Q;vQM95o_+J>t#N4fQ-#&vG-JGfI>zt04^wsWMm4)( zQcm7mO-92WuH(VToMv_P%_n1sWv?*mi>DZ2JiAk{*tL_zzMU*~o;!WBFC$3&Gs*2p z&?F=qe;kn&#Q|AS9FG;n;aE{M4!-3=p$fR}JszfWs>a_QDblZF4d-=ZCnGsp?(uLd zr-f-nW6qta1h&-}`|E~>s`scqDV1KzRO@|tD=v2a%A?6&uW+%uo-p>-6UNT6buUV? z0<*MoKAXWTZKxe>xE*bz9c}&sVgj?V`NzXhejFAfcbk^_-Ox9NaRKwARiC2g$m$xQ z9x&4VoWyqLq)8n1Hq5!bZ*|3?ST1q=KCED4G+DuddOaG~E=Gw7%x7Ug2dm(~Jg?i5 zH`J0h+%i7@7?;?nnvXFqnU_*EN|ev1{&*Pc4A;MY^xWfJqPk4uGn?$H$HQ1@pBw(d z)S{rwJv%dLQscmlr=eTYKCA>VJex$YhYfb0+Ma~j5W~jC>wMmm{_sU2O{WXgqQbj4 z$b|Em?Bikf8nCxavgCo;wbMNwMhmpWMN?>jJz*7?Z@I_AXo2m0yT`+5f$g>3<6*Qw zKk*0|%&!l8&&xk1#=@V&zIdX2oNIK{m?x|r=OWouTk4N9IwWYXe&c9zXQJ-$F!so&zsh?vMSJH#aYVMd z?(wkwr@zjG(-d1=c^-nsb*~=k##Kinmt|b>>ftMO2I~)>9b3GWM|`Z%vqg%j(IDy&arDB4|h5D z2YV|Kr~Y71$eBOb6UK$J4~OmlFa~b4Q33&TZz<~e?%V?urFaDp^J6N{P1-AQX~%sa*&%K32ob$o1L z;BfpS=Q%THhA1(Z&!T9_v+qXKkb!x7-IhE*nA~{!P~VeedLIq9uP11JtdHo8RR0zw z2Elxg?D{3zSdqr7+2C9DC)s8u*vmB$x3lZ<)Ki_E%J_#n@bL z*2D&T!q{YXez~7my@!n2PYA?ux(k{(%*+P~hI-$&pX1E)Mh6elkM_GY%GGF3T&hw2 zSs3jJa$Zm!6uo-izr|@XhEW^b=VS!TT@sciqu6ITbf(g9FB>Ra|F-kp?V5^9qUl<5hZFUGHr)1n4%V zZkFKY^5ABYCWf7xB~$b&H_79lyo=}I0!?S{dCB#o4?c}>U(~-FB3w?hZgJn%7GHqH z?+1%3Xx1&hAhFo73*V7anhMI}l;)O7|D1Zb_tKo_7l?_z9V(Q+u1!{y(|pOmn$Fs>(q?3OOBWpm z#{IVFgq>&fFO*V`%%Bi%PZ$U4uC;OXmP&i^R7N{}hpn^r3TdHjtAXxkw5g#$Zx~q9 zu5{9+BC{_~TnS^&kj+m_ANKpS`GXaW`p=+Qxm>4jeEdATns|cdQy$>&f7nwJnopIe zFvl&Ty@zC_dG)iV=fvVT}qRH?8nh) z;@2;^^qv`_SVnUni9C_sb1NdvrF}$t&##CyN!447v-Q$?VsSf7bP0(*EqI%xNovmS zk|}z`U@rp+V5d;-Nt%p+*Nx+#f0qirjhy%9IgTh;Pl#8rC&d4M$bV0WZLnkTvRT~= zn?d`lwhCsJwB{qZeQqU>wfq)`Le}zIJVjohO=a;^Ccku*^2g&j^M%h|02Bvo^5tp} z51W&b4d`Sz0Q2>Gh=%kj(+w?QIowaBK2m5`WtwjfqGdGCmB>xci79&TsfaY!^bwiW z%M_JYuZPj!Ca4pF;iWX+8AOvb3By@k8iFDMyT8TmGk0Q~{|&G5YwSOSwiGl!dZOQy z54>9}(DvoR>;l?veJfE*GkNytf$AJ1v>%pPXU-m_`<^=Y1L_b{&p?0u?4zYFp%dD? z>LfHKmj?2dvfX#anzbYt6f8kOhEhYraEKN7pn(806~Gq#$E-0zKZXwb!Ztg@>@86%Vnw8?(Rs-6+;L$sM> z`ZB|8E{zgjr^vz#bFVP7R4AucNcS>R5(+p|NV8Qi=5!^?%Gfk*ItepV!?F#jYuI6? zTjYx!6j(({Ztjf)OOP|G=O%4P3j%3txy@LzKa8u5mAfRSq7A8RL$VjwNePm>wM7w= zru(V0qLs;nlv~@&#BsJ_?3Mkv{wTj_{7Gm}5NK|9tlwm<(R{c>{+xl{n~w`bn&(L5 ziS!;H&O*j%o-2_j()&b3q1ZLBF*dii1dC{5&H{Z&nxVQlkQUb_dnXFVo!M& zPJKq%eo5F9#@gtx|Tn%yDysy+9I$AgavLqN3uS&hnlkM~=* z5!w;W6tG39njO1|8GClJ*tL_zzMU*~?qoj2>fd2A91`BH8mKLU=g3chRFxv!fTgU`Om(V8}L_u;w0wf)dPOd0=P=s#cZWKEmhgP%bydgUG|{l^N2%t`LIg6Qreu{}+riAOPUc|%yO#%SIyk(hJ| zE_3Qvj1mu+|1e8A%yux(>$c<#wd4)AjL&`ZgUF~?{|-VC@^RO{If5OA7yf2F^Kc2e_2-ES9;5xkGkp@+!3MiVZ7;#>oW$z<-BZ5sLjvx` z8v8g^$=__dO}jg+%>M0OB*^{%_Lewy2Qa(txo?V~1zO^wDYU?zkU;|7H$~6_+xvFk z6hRAYukF4mf)?l}93g}Gy=Jq%7u;dttzrK=+J1-QTJ>%g*1yAn4DA072fptgjvCJ1 zBK39@R^L29@^EdbfAa*{V-hXK0rcM#A(tZ*b?>8)bV$%%q{cDkrl9+#2=;CbwQ(mP zXcJXqm9upwoTk{~%KL9boM^wxK)pdfCU%ScMIU0K>MohzbS$-aHEY92w2z?{MdEZuoawd zSM=edo4zT+%L(08EP?(ltR42*0iA!-bxCQKojpqD{t^zEUp1kC zTpRbEGsKspJp*xy^oeX+1z|tuyuF$PU`4S>D~d00Gar!dm`9|ha9kldO^BS|r;8FE znCF#4YzG@Y$j)Z4;e+gK2J>Er!;G0ei_w93nR}wCF?V9u@HG?S;?9cCgWDO8)}54m zc7Cj43pOIuw+=UACTL_EJL!%;p>A@4l&EcGqRpG2w>F%H0+FOHT0nqdfq8OU<8GqIZ@oZhSr2=q z7rjDgMQ>2^>`HWnnm1LVtuzHs3A1zM>1uL-vbN&;=2jGC)ayQ7nM{U^mJsWt*k3$tZnbDqxs-FebOlqD0hCyb|NPZ&?mp3vpf|95~K9wM}lPOtvY%=3Sz1^3`8NSdf; ze%7iJ^dL3)a69W@K8m0>lMgqG2J}&48*IwM?YjOy=gs{+U334mu+OeIdz7)9r*)?^ zR!E1Xt9PEwqV?fAlg>pFcI^f9*`xGW)V!45&*}zP?+UGKJy&#C0eeCDBy#^At%Qyb zJNFn(aG1tTHhAu zTi+JvT;CSwUEdbxUf&ky?@y4cas83Tx2|({P+>$CHkHz9r5!Tbqu z%FA4qbz2%6YRT&q1O?`UvxQTsF3_NTOE_#VXWcK5)2D<^?tU(VI_{(83O@a6q4E5< z$ncY)Gv_J-BFfa>2MvMHW;wYM&%H{-My+>JB_%70v&qG1@9$lrR6x7Row~xFORQAU zK;%hC85_MUQ3SJlL8mObcm~^RyTge)xS+{Iu(OwfJUJYyCJE%FK{S!pDEU}bvyV{p z%Et(0sti#D&>CfdAan`rGvL$pus8urah z8B@{-pHajyHS za3;2#Q{3}{XpAOj`)=>oR_+pJUVX%Swqv)w)MLjzA+GV$6ULshmFwka6OGN<+cJDQ z^x`TXn(KmSJob{i1?ZmbB_4X=-~)s019yYNc-&cy3(da-QSwf#GU!3FGx?_6Ee&_5 z9Lz^I=*{G#_nc0_61?3JXri8*CS5jiaho0C!nC55fmViF5cb-nH$Q6sbOY_53Oei5 z6l@0bG=sBgQ?S?aifN(HzEiIjly4S+HLbCVhB3Q(#KzpIFFqH9TL&lF@8fr-=vV4X zV|Duye<@8HN=2(|C84-Uh&5chj|sF4#dHY)%#K9Sj#jp#nUZyp`yW}S_M0DCNWd(u z(~efOqYWDh@vtzk`Q1NdX713Av^JMAGv?e(+Fo%zRA`q2nuMIWzH$rVV47~S52k59 zxZV3BEbe031@)J1LFIMa_&=eJVWliRR0$LBERJ@nSxCbrp(+2vW60S zIad9{uC507uAX+S;7;*_!YS*=F8$4DN51JLl<7GwhP+3tm zKE6fV#LMiMOCO!`G1^POOd#bb8^OYyM06qQl+`u6J$;AdOn4-*W-ob1hqu4L?Sa|9 z$7_|_Eu1!u(L6MW#?vB?r$tU4U3rIiz??m=ed&9AJX5jV`nK3XeOsJLG0FgfnNkn8 zA-T^uW8=ADkqa~S^lB2vG1!;5h(9iD+yN72CR%qb@Bc76{XWGG#4{9Yl4lXq?^Cz| zhucI~&Cq)|I(I;rRDG+0tV~l)9)}hsD-qZ@KEp7;CZ;hv>zU^6!uHJEa%#*IB72z| z(496U`!rjY_7i3-4F%FOn@J;WNcQauvT%g3xezfkUx;|C-2N#%ag2BAq8o7tat`$HdHhWPHQ~dzmx6j%h(E3>MbT5LtIVOfa+F zx%;msn5>;FWEQr}nL$sx_RmNgpYwVO1hvsVyM{_!Usa^x>OI5!NA5a5wV4?&m&TQH zd7aRnnbRbi>>NZ7QvNb$UW{u-TDpB7!dpv^SI%H>Lfb8qC{TJ^76QS7IXHJdY^|~<2F`nI!`@$y)pNJp4L^!Ys+YAy}K=RnZ=;`}x(U zQImTOu5hX51e#9@&s>t5*e-{+B|h6|7G>9*pbyxBy$>}1K;^hvnEjNq-6f>^dSL_Y zG;QYHB3YWvL9|Z_Gmj4JL8t}E9%wI8J-#orF1MWG+fy&p*F$?kKLe_NixOQhODl)i z4CcOIRt~cjw5&yyGv7*CS(n2bm6vn%>4B?K*Z!WY*zuvFOI6YAkX3%soM+z=K76ck zKgSeXsBQAa>Hqih8cx zn4RQBJ6hR})@?^~XXZrp%WB0~ICZGz^((6tW8qZ)jP$)LrG>VgS?C@!{l0Piz?%5# zYDu*99Gzlq!+TE{N9;y%aBXMpHbsqZn=hUZoIc;b4yLSvacX21XmFCwcELbNiPu|0cAxpt&fB#%b;lL=!Y0C{g}! z-ZL%+c55!S2=A=vBWev^V{o#rW?SZT@DUjjvPl z$CW77@?W@nw+QbSj+j-u4j*l{oJP!Auc~gk)c{q&4-cYonrkHT!$t4%ib!)^ACcY<0&#-o zzSrV+oaWml@=jj5#uMIjp2vonV$PTCFIPUiLiMk^&2EZ2qKxg=sm?fgiln!x)M*|R zM9FVEmBAV$dspqE2l z=EhTV$+?a{RY}ba7+sUO0i*NPI6<|0ZJ1GGG~XRWOGtKgs9k26BT-B?j2D~F20yP3CvB)@||nE%9Jom zVk4L#bxJmZ8B)=P_7wBPH3}9|4htZKt!8k}?N+EIcJxO57DDCN-tlO_gX8ZViuW$tPnen6Ys?-RcV=%(QZh+8 z!O}iqCM5^`94WC>7%UJ|^Ysi&Fc*D+8L+WpU6uf_X|P7-VGC?HKepG4ss>jbHo`L` z;$D~)aU<;5HGUv-dGxlV=EhTVzUNtfYJHW<<#7O(fKOaH{PjKhle>eM)UPS zw1h%}_I@lCI3^S{v-S}#UK$4x!mEPMW%vSTGl;P{cb$VB(8)-GS(0dspod5T8$Ey65`jbMhUo|`uatwoxr1<^Row*}Dz%`+s*KM!-pr*ZMdU@tl7yQs#rdH+D0 zpn1TwZhI-&e=gQz-EUM+d}*!=qH&s61F?H0^oHerNiTnTk!+x0xCKR$uK2?-tsWX)gYe-((}Ad4NQ|fb)5Kn7eolmM2>p#1de>I!GU!Tb#%_q8(^#MB?*_o@ivaO%omsnWBzsOVM7$(h5vP4Elm zBb?uo=WbEqj#ApGM3aJL&KJ5t4^mTAjk!)~{w`D{8KfL3$=(<&{Bb%?X|YEvmuI+1oac$=srf4Q9L!C80GXkutMhhZBPtuPLd?Ys$=eH3hkX z*5L2Il)Xg-1I)}`W46_}GkaT-GLvM7$WqgdrB3jPM#g83l=w6S7Ql=pQ;-_iSg|fk z0N6BGBlEBYHXZitsuv%8Y>e^I!-_a`fL1iFqsU5OE;+5LsgLp0$2hh@8+7U;IrH;J z>AN?NW*>8xhcDF=HA|mft#>q+2hkGBvLci|vvg8)Og78`HjfAMZAUiNbhoI8j7iaa z+X`*cv&$3BDwH;vy24=$&H8<&IHdYE&CdWui6yXMR&S}wKQxTM?(6r>indd z%Tmo{AtE)(p$E!5mwYliV-$~8B(_mX{ZfpxD9D!9`=Ct4ff)--C^9 zRI%Ru9y*n7vR-epDVks=iUn;*LnF8WGc_KW$3`$iGUa2H#gm1Zm`uq=FhlCJAr);% z_G!3cIXT3XOjuL05zJ)QX+tX7kW6`~VmTtjluQm&vJuSIiH%@}RJ5{W&eHiW7gU)m z-j4a0NL(sp*6EbwEqK`@ zoW{k2gT3UC?cyFx)1|iIP=7csf`bW~<%|Ijtn55je`AOH7~U?l155MoL9~o!srxOy zj`SW--H@R<9z@G%E(@XwnkNKNy4O`9nmES79AgNQ7)tYp&QdyyWY-aC6Go3nXWtr; zHbM1>wJ2!XF<+_5tP)2|% zPy*}9hl$oZ2-B%_lQnviO>qn616X7hJbXFBA$GVDke0DD5v`#ay9p>M17;KPHe2OzfXoWB5|_#uTx&G(7{ZViZ&$E zC6QY~F_NiVm=>fmkgjRA)D5Jonn|W$Bbdp~7~D(~ct!e!+~t3UrG=>?_`_7@YTaGA zV(UhE*E~R-%5KYp= zu)iRsSGh?pn<{rWA_@C}Seh3HN6Tnp+BsS>Men~VB29dIV&$&CwO03Lek8QJ5Y6>L zG*0uTAex|gvqZkW{lD~QGXx-8r`k~*{P}O3CRQ@K+ z^Sq76yvO4Tp>;#^LqW8R<^>Y9vAAzgUrO^MK{QFTZgFW+Fc%vUzAz{+p;=d6k}4OE zmkOAR!tWf8&40UOpYljvsT6=H#^hmP?(sPIiJmu5|24j zW{EmZiJdrIf+;C+XUs{mFm0()QY%YI;P#h`W_Fh&CGK*h#GNr`%iUC{Q)Z7yNy%*K z1WWH}wv;0!mI{LfVru$$i78-$nH|Cum;oF2udu&;T`AlUQkkIn3yDhe>Qz&8epB&i zE_<)HW^a$soRny~_1h`o{pMhGDNP(Y6H9~pKU74TF9_}@Y1ZARi+v}!g*R;e`m>L9 z(nn$y^YY*~Ij1&;jaSc(l%C2amYM=9y7&ZhXA23b$y}K*Gv-oKlesXtY~$~$$wG~i zyXGu;o|hJgI^FjifdG8rytEn2hsR0YLZ2+ zQq~^E(6paldKSM18k?wZ^P%+ryZS2Mf1S`4gyy~US*m<3y&CH84C+g1{v?RTY1S>K zXP`ytZp2<7wZ978hhAxS6p^hSO-$xL8+)3JoGY|XKE^U9J25S1tYczU)>ul*87tZu zOKEvyq(|*+Wcwy*7AB_UjFky1Yb>SZjCI=?OKCY{1287uF$`a8nZ|f{|R9Sn4o!Z5G|v5h(taHddF2nny>03 z(gW>M@PYUFNIoL8BAR@$AfITJ0HN;^+%Kj1k{}u<$YU!JVSx7bA>n*HgtN4A9b?=(v>;ZrtDUH}m^@;861_V^!)9I0BpeLD?L;>1k z;(-+YK)<2kH$OlRoK`>eVYp;_qW@Rz4SRkJGW_$2hlQ` zCrRXYQGPY%5jrx=wBx3}mg4J(;A@=b(KTO7r|1!ZEl=#ep<>3QewW}Eb1w8HQ?W%a z&6VvL3JyE9vOPDXlHB%mGifkQcdkE}rZFoN&;cQ^J?VX8sm!NM##R$1oBPuEP3!dU4S)57%~v$%dch*fVh>fB6Gv2^DPH#UEI62s)osNZYQ=J6wC+YfQdbFWQZ%oYD8J6Rxihe`JHQUC z>;v}yF2330(}XsBnlB2XahiunWXITkb)Mc!gVYTky(24in#cAL>tV!g!g_izvXtij zL9~n}f!Gr?nndAhzcj>jRHaL^9@F%v!D&oOi0Qy!cakQ7*bi*btjDw@#B@xhOS2wR z`XJNQ>KRgcc2FL}I?DDEHLj}f?=2k$)vhBniFCU0o!%%zVsFjS#AxH_OmC5b+B;e_ zD~k=aO)ZWq=}NVsI#T_G$9oCwZb9=!K{QEoWe`o!+)pCE@6$Ul5GQFK)JIGMuSvw* zIv80>bKf8ur%52T>NJVM?-TT1T`*UV0p*`UJ#=E&zXe&nZH$gN`^Z465+ZS>DdA_$nI9+K|KTGjbH@b8xXEfbk zdI$RE(66gP_aRpQ_KxxcX}YhQV_4kkDN2pIp;m1WqPEcTJ2=B%a$?d28Wr*z+b_!wu?R|?ZvoJ88s_2GXcH1wi zjQQ5kJcwWeaU>zm^A?A<&%AZ6OA)~at3^Vb#%fV8R9z*NpZs}VI-gwy*6ICne-PST zk>;He<((MwLv6n!s3r@r^KiX3-(1mt(N5E8=l?dG=&q>k7s+1j(MC=4ok6sW=KCe` ziO_q`$>=Vl`9BhQBE4@^M4Er^BhnjQ9W2t^P9krS-UStr=7oJkdbd|Znk!zT)O&j? zy#pk*OFhlA`iOBIJH?Qy0@__^3->ds@n5I7pO|11)4q9L%-;&_HSyx)BCrg!$0tpG5g=x>QY5w{o#&ELKAU`mS|KyX^B=ClZv&Ky0 z;Sa&-z?3|hFy&rDwqI2emjlA3yeVkbCD&NgBbV^mdG$)QuJzQ16NKfe4 z^*=XT2G@2~om*{}VH|i<)?|C1D(i()4_4L~u~~ChvdY_KJ-6B}#@KZ}Zlc|~ih9dr zKP*fuTG@`)9l;uCtCBh$YDXJxM;mDuo0~!}QYlUhE5wTQps}JFKHn;O`!r?5hFwZ9 zgAFU<(87@;v*6Ut@1lVA65550=E@*ij-S_ru$R+3Gl<4$R?#d^qgh_}PMehQV{bBJ zgafD*!R|*6#Zy%vG_hfiaq;2Riu%p?9>&nL$C+c_<=6MA*hGC>Y-=ct^ueKunfNJj zMM}!kF)c_%I}&b=4lyO}x+W&&-`bG!GT-qUYH2LxWsD8CG?wx*#;Wn679R;U35W3q zgc&vYY&Dlwq8F%H&Dlw2O*LmHD@VCj?A+Iy(+K=~X&i!=#!+}FfqX?VdMUljLJ|`+ ziP#f!p*q=HRHM&_S}iBmZDi9O2{`9DH27Ig^Y|Jq_F_L`C%gE`M`M4iI^typxMNn& zOd$8)DEiKMA_tj%p+l+^} ze3_|O8Y23-qRk%5F4e}0lq}92(}GmAA-QaD#05<6teMHdB~0U~^xSIOvc;aWmE4%9 z-OP!jZs*rk>wh&oRLkB#AmSag3tN$6_WylXdOr$dpBdI4%uI4t*AEnfTYGLENYiI> zw;W73;Cx55>z$_4xuPm4&2@F2kE{B(C@}|S&-%+DHiLFY-?B2}TjelYLCab+Gb`)T ztNm2LUtc9LIz{K_RkAd1s`Fyc#<2M}K{Ioj!Nxx}lS%`L)AZ?YtH5q$5>oEoOE+}ykNcKyga`RfDy?wTQ z?nav__SQWBa8pw*+}z(68{U3VamV`bUgi?pbF$cA?xipNC8z?Nj1Ag( z6x+*8EuTLXt;~&M?j@mCl;qGThOA;lw*@2^qZpANO0pn17O{-FT9pHxJeKYuMZ zqnFXVRU)rT?}e`mM4FQlc_O`)6_Mt9`-t?WDk9Av_7Uk_UlD08JjF-jUDLadr1oA7 z%`N+g^d3+VX+E@%NbeC9k>*SKi1EmD45M;kRqU8#A8cvkFNN8s7=Br<$mgjJ^@A=o zdbFadZ`(z(r)9P+k&JBWdU3c#5^nAoontjWAMrj&|F%%8F`7RL6LXv?xM`RNIsbTN&Z2YXFh*YPqBl+~ zf5YBBno%wO5hn7{qf?U_ofix34Vcm7)WXf~!L?mg=T_Tg7zf^zHQ6rEB((`HoO-^p z#Yj&W&jy$E+-kcRW7qk(iT1u#)ajFs!nC55?P%Q*tbw*Fsnel$wBdHNk#@299dn6_ zaZ6Zlmb2baReONuz`=1LbIkV7T-nj==kdWC5#8f?9)Pq9ZRi>HQc z#%N9i(PSFUB+>jl1UN?Xwji41KwHI}xsY`6korbfmE;KG_aT@ungnG}gwp~}rVD;U z$auh^8_|P9p)kwCXr$+whM}rRNzC@vkt|F@D%z2-aA=4rao065 z$$qo&S;?B7(lS>kL+xy(w2ZCccD7Pl#ukHQ423v16eOI36k-02KSIr&E77iMRx8Ii zyGpfkBqtn|W7mqE_X^FDg!`T_4xlHDL+A+!Vv)}JdO|uoNNA@NO@i(5`opVwC#gCg z3RRjU%&zL-jzgTP?H$TANs}_=4p&7e1b(u?lnE^}vK@1jV#HR4L}F0Rl!|+i19|-OrySn+reqV-hn=y)rV77^t3*kz!u;gv@Tz&5i=k zb?ymY?y&9u!`pjnqe7c(?Lf!5)+Z&YT3VcF{skV!WIV)$%gnCQ5YeX;ZRQlhn2%~> zMM{?9P8(9uhU8`lj=0?FovJe{Hq|ul37JozJCmJTZCke3bGDKj3l(YUkhWO8P@-qndD;l&OkA^+a>D;(sVwJ;Wh~ejovaqhr>qg z2=c)Ck;a>d|9zDzc5;1|(Rp1}PMW9Gd9gA369(G+mY~^ipyeF=@XFqnQ*@3D+7mR7 zs`FyMMTt2udx299u^F_>OXf)uTS3cO6uT^k*$QS|+D~67@N+^=Nkz&);(~v=8$lJj zl}SjsyG7l^apq1;ocV4C`n!Hhgm%8sJUocTX}(V)e~&Qq4&pxMcvsE;5PBlL9V#Nt zhx8FwUGC#sE%fSf%=La6|5D;VFvP!%W{B8+414Jmy~`^i&3af%r*dKCKbY@A!`U&x z>=?~=1ko})-4H}$^P}SBJ|9;Q^tXauG6PM9l=-CHCu9Lj`OlZwUxF9!o55pdIsVej zBsWQ3DjE~Y&QO}iYE#pg$Gpts?wA&&q7BK7BLUS0jS<>9Oyo>mjd_JqsWit^&1Bfh zp!XW-XHs;h2yDiDX^z`+Dt6Elx-{y4PTT#(7W~j-;yz)YbU!IZ?*Sp_r8Fr+)66+d zd7LiH#$-%P%YJn>CgpwFw=cEvnbI;oQ&Qe1MRkQ`uVx*zBS>>zsH*$&Ha%*7w=9(1 zQQ`qJGio>?*<;_X$$bdk6#q@V-2FR3UK8)KumACrgj?uW1Ro&emkjG%*PbnZKiNB2 zc$n~bVQhc4{EKDhbHXnQzu91awtS=9C?bUgY366c=yK zj{PAAuN0rRd&m4=YWp9Be-ZL_=$PNc<9(np|6cF)lKTpIYcd$~c@ZwiTX4Z9KHEWF zMjP{au`0Ni;^L*JP5hYGl<#dCZ}5-m-`Oxfw$u9vcNUHb_ZPss(!14 zM+@H~e4p@qVVuvmC0{4}q3}1te+awpi#2`!v6Am0e1`DZ4gBXxzMt>_;Ss{s!Z!+I zuXpG;{!bzAc)h<~3HvZ1?|hB*`GHq{u5OL^{21M+ek+O}Ol#7|&P$}v@2@uT4~Te8 ze0Z_lCO+~(^EpLvf&4%o80P~Iga5UQ@!~)RPu=H zfgU(pemq}e`wv$A9xmjE55bsE9N3M2JdocUY!ZDv0mGYWUUWLjFC}Tcv{^S*ytMn>NFn;YQ_Q1Q_9&hrTtN7wqev&6R z+k8Kvb^6mn-sE|O`s;A2Nz2*He)A!F3AAE=U?-s^dpOgHMmiFO; zmrDOD!fy+|7vVbfoBYq#&W?)rUu}=Pn&P=md4s=J|91`bfcNu=_s0f4JnX`|OZ-xG zx_?8xV&1mmHRTiYcbI{{SlgLjp4&U2i4zfBlX{*{N5*ABm9E! za^aQ2IDX!W%qx(o6K`{l`MiD@+*|SUzTZ)kNsVuxSHaP`Q|5Ha-@)X_{rqSfi%TK zZ$$lP3de-|3$b^C?42!~t^EbE*A#yo&s^~r$?p>f%x4$n`xN2RgfA2xCOldg=Z9bF z`#i-#eVh1uh&LfzE{yr`un(`vKJo))?-a$&xW@KZiwB;f{#gz2te5=Xukn_cC*NtP z&#%PaO#IJ?*TnyW_trj(@iNMY3~~kT`B_i2o7dJxaKvZ4X z4B9oe5J+%&?4 z>U;0izk}L2=>Ld4@@k6b-xVkLF!dkR5I?-_Bi>^g`0%g`Z@huOujJsQ`Y)b=AM={> zJzRF0=36!LzIB55X9(l@8S_82#!J2=Tr0d%=(kq?!vCzce=ov{>08*lOuTOi(ete8 z-=8S%cz=xZXxh&zOTL9Y@LJiwQTQw2ZzKG@`c3v`YiCUB%fH$lc{Rmzm+}VxO>y79 zA%1w>h_`J6A0Bq$J*9!ahveWh)!%0Ze#~picVF3Qns4!V9U%Uzgd8uJpD}+(>;Juk z_Z2=!*u~(;z{TqCDU9tSAJg`w!u=cg@W8hGn0K!1pS#{i zba901)n9aj-@d2t0^x0(0ua5k`Z@t$cjxVT>f3ULbTN`UXws*AR zYKkxB2cGnr@|df7o=)9_m#_B`VgIokIbNTX-R}xF$X=&meBpzaNbk$SxL#k2?B6+q zed73$^q!%7j}pFB`tOa9dHO8H%{*PD{wo{uiTxcgOMhb<>FILpC`zwVe z`lHUjR=w||x`J!f|DkZbFrFv(RULK{G7gbD6Z@O3{n#FHm+So&;%5CAQ@r~&Vx0+fjmEi&kKRr4bB8eI z|4nl2?>K|~uEr7mPBZXViGPgn1mS979M2lv4+GCv|BJ$fM|65BTL|Y`f42TUJInaU z<4WlnmJ4<}Zx!$F4Scq5(01^z4SeR&HJVS~6pwin$8)`SGwmOy!5{m< zP8xTxX@9&)$Ddn-zZE{<=RUIiga-@bc$@UG6Z4U`wZBk)+w$)>OFniNOK(@p=f_P~sUPd}1DD`@{a_+Lcp3Bg0mkQx$L~$@+WX#=gNLtfAHe`h%?UbC9>a^f2{Z)6kZ^_ zPA`2L76wtsi?-#W{9i2Gvc zZHV%S?Vlk&c#is?60Q|qCXDm>_$>MS+%UiAOWyqAa%>+y zc#Zt=tG+QGIX`B*P(NnN?+q_jpWhjde_wc}cy5({e%}~-U>py;4bp=*sQ)H=*p2;@ z_jV0=x8)x${g^-cTQAvGxV`W}!iNbTDU9{MAiLm~)&GX@2g0j_vHls_&n^~zMtEt% z{x>yC{(g%4*xS8l=IMPt7)Rp!@WF$nf2i;{;hTlAl_vh`2LAbye?z$C25<2(!Urnu zM?~?Dd?5Dh``EiUvbU}5#ai$`(UOl{;>9lU#`fWVUiRTXxFH^R*oW6-|0}W!e{od5 zI39TGq=#K_Kj|MOT=;u$VUchqd-t?HdE6Z3GZy8sTKzcwne;c7Ph9u^owqcqIGE?e zN!?<9h4k(voQXd&OFnj+;)~7xQF|=F3{)df^5k`}O~7KfbnMf5zWa ztuFKjtxh__?ZKyqKpqYrF=vf2r()gZlfm>P`HY%kH4| zn3o`Vf^oi_$Bt>9Zm)TFl<+Je^E959vAws+&N~_~<}dt%mpt=N9+~GmH(U?gTl{|$ zZWG~y)PI&R_803vLA>1>_}F`>wm({kUTh!UBO_iD{{Zp8L;mP3f>)`3jWG6y-WO$O zH}S?II~PZGo*LDA`-XaB{{-15u2)C)Zcsmt54||qysAojqU z71x~&@tvZ$i0@6}5#Qg{ZyVp6B70{=_BzU|tv%vu8sE4+pOXK!{BMi@XW=5n%RGwh z!`oau=Glb$8_S<3J8kVVPM1jUbHcAT#51P)|KKk^Qr4MIogYW;`|!d2rGI$C{Jd21 z-wD^=;q9yw-YHyoXWrjWq<4!D{y(G_Yh61_K6abp!%tIu=zTQGYkid0gz`H?_?4*M zpVfMEg|Mmq_yhkyU4(HxV%|T*17kk&$Ig(?nDRJPc%Jh4gm9+(&eZ%mU)YrIbN}ok zTqb;xkoc}voN>M5_|A;#{m!V~#1q#8e_-q{<{zc_zyZ;1b$8T>Dq!T)US#rcyrNZxCee-r;U#eKW*pF;H3HRz)U&Q||# z1Nw{oY!j^y%-1;IrZ{8$Mama!vPYeU1U`p!M*P!kvUq7A_YaB#ia(!+QJ?`Q!K!^W*h;N__NU zK5@gFY5jh-#vgp1`p*}}_M3PdUzum|@sxSd*4`2Fe}d-U|5g2i6ldFb;k`wAhbTYt zjO%lE^Ixkt+QtK~X}sh3r)0k^f4%m{rQJ@?6z3glFODtyK78;->Hk*vSK;FdZ*NTa z6XAMc>~FUZdHdtS{e*87epL8b;SYp236J=Q*MF7pdf^?y<1g^rPZ1vUQQtpSxWk3M z{}|x`!lQ*x{G{Lh9O3_O_4(C@ef*D5J^p{I*R%hckKaqHf0=)GX&%m#-e$sz>03>_ zE&h-2i35z|-&S$%D2&H9=095Fzl-om!rg^W7cLR*FFaHj+k2|~g3nT)@rwDA+J2z$ zE2Up2t`=^i{d0vd)_<_%I|&~n+)cPd_yS?9ze;{j5WZh|v0mVp^&-~4K>SODpA~*Z zc%|^C!dU;c@_V}QOkwd4&zvXRO1O>ivBD<__YnR?b=z9&_fEoiePvx|JseVfexmj7 z*TUC*%*V0t;~tj?4-|e#_^;N&w3p`dS;7y^Fu$=6{$6o( z<*zB8nE&U7_!r2|{bm{eo1^$Yt$rNOb0i-Z?kij&e6jFw;Yq^R3uAkaP@Lf7)PJHd z<`XCNCeEhygmHWqb~T=i>xO9kiv6Jv?kzj}H^c`I{D0h?2Yi*){r|5UH!3PPbGTi(wAqfx(Nle00tI^_U9Yxz(>ZsIFXBAh)jn>&(i(6Z*xY0UrRIOV5 zpXWL6&&|Ex+{3+>W&6Lc)=!@A{d~vyp6?mYd7cFEM+ErCA#VkL7>KWwd9)um7_{;J zi}rj4eh01vzYEw6+y&eVwD!&>F5xe@Rz5916sOh)%b$zAd%*j_uR*)M+4V>3iN>L| ze>rxp25$tdzlV_L(pP*!%P-Xa_r(vz{{(q>4h-f0ZRT6$fB(SvzE1lD@HX%+@Luq7 z@NeL2pv~_G#3lTkYt@V8D^BfK6lbVjWJmZP>~H>!wd}9O6NC4F!TI3*ppEBT;t*cQ^~Ip&Oa3L_pH7^i{cA{Hb}q-x4d6}Syo2;I-gg-~*uT zueXUq_z~BiftIg0RZogj_d}Mi`#zz)|65+Z#-;Y<8kfqewV&(0RQIDo-IrEze_9Pr z0!z4G{Ryc1(oX~D_si<=Ccjs$JKq$iaA{sF$^`PyIA{K_2dRX!{) zl%FH8HxA6z|9_w^?gkftkARPZ&w+1)AA+`jfq= z#^F@P$7kqkpQrP4or~FXAI)3Z_u2TyV%Ls)jepCZ0zVW_q5d1$H_iZ01kVKLfOA0` zkK(w9xZb$IGeXY?Li#76{}b?B(DE-p9`ZNLJdd#Y9}wRc;8)<<=xqsZ4_bZsT|gX{ zqpx=B`NuivUkCmvhrQoo?|Sei(E1PYR#ZIm@f)(2t35Z6k3WESf)9hwgZ}_+`{eg+ z;?VP_Uvd6=2ztu9&2z}!qu6^2d=9ky5N}1r^AG&Wo^6kfXC&=C7EFSZ!IQvQpw+ke z%at$1HR)S#sfhVb>$CddCHiA$&cW^T!Sl^I`PzZ|5BU@Rll-iKf7>4MgyJs=^ym86 z=?2>Pt^G@A*Kfh!fj59Rfp>#XfX{-~UI~7M{kT35w0z0O^8NDWcMNty_VgSh)IY`a zV_)!x;9&4ba2#mcBfp_~TGf2T@g8~UN`0%&^<3>;kcxyUaBZTvP~>+eGP?P72)_&e}g@OJPK@JZ0xyM;J}cXPb}w0y~5<$Ig& zT={(gy^#Kn^h>CJ-eBB*04@Q)0XL+bn+Mt}zoB}v?OoY?#nJE`o~dKix$Yw`MgA@O ze!Wj(-xv9u_eJ{fKEws!HK1*u{0l##f7VevMnOEG_@4#pWn=7g2XnQ@xJ#@-d%A!d zf}4R`fIEUc!5@OwUN7Ph9>Dbg(DEg(=KJN%_fgo9y+ZpdK|ihsZV2`O_W;X4+aCE1 z)!VA(D~=Dz%M#w7N>J~r`y05{If}-CeSb>tOWC~Ek`Ijoy)UGBM(4VEj-zv3n-9e+ zJf66J0_Mw~7T}+c{36h{U*ozW&>rjm_wa53?*Q)w7l2QIuYhlX*4`h8OL#xm4}q31 z`5Sz1`|Ak&sDBFWkH2GA_6yY?%6fA+cobL%P6JN{ZToVy$MO|tsNPpK-;O8MmHNfT z^CkKII@RC){igL29#=JAe)q<|>PYLJo{QaUAUTs9u*2@B=_jP|G=t^GFq&je30X!%vx zv-KFNn~;9Uj^$sBzbnC92n5dN07*TNgX|+Ozswv0vF4 z+!gEz9t;izt-kzjLmc{jn8jTGxAPUZe!oZiEd73u+OZ_io?QC2{e|+!@&080E`A=+ zd!ZG#Sj+x;I{cHtvkY3Td3@inwzuC7?rZ$j-t0Ah1cv;}-bZU?+FOX-CEym={{d+I z9iFGZweYh6xCto#r{mB1U(K}7`nweW_ktyXe9NCu{$}IX=6g5d6Sl46^E4B*{5JS! zf(dxt!1>@KIrL|sFaA9NzIZ3WKQ)K{2Xgo?)Shj>+AUPOcMHTX-W9}s4R|{!y}ki` z=?M$fzj+z`SwGKVM|Gt7wfPCfY4zt)kHU~W<>_Ahi!ZePAA}e3cdZ$JR{zgA^zX=_ zFTWb^OEm5R{rlg>|C~5AA8p7y6Pj-xWj=Zed=^{;CeZH^n6KnV^Y2yY{a*T}gY5h) zp5{eEPiWqhU9}^$UaUiZ2{+(+6ZNBU5aMb5QeCI&AW!|>@wW*4Z`I$P`0|Zcyg}$G zKWe8*Phw^B_a}~g<5|exTYd{%0)7eF_+KrMpJ?&xZxOgNxEE;cZ(Jb1ihagG>v}(f z7?#a~k%}1TPKPm;5ol{}SAX_Skr?;`?jBo4`MS-x(}a z-}3Lpp5ohtxI*zgX!u*?;LGj{=q&;lgEpT3Hoo%E$JAd{p#H?O^{Tp+UZ{S{@$*wq zyyk$v+3;>&&yNrBesrtNtiQen{`KHI@D9*wS>9dn?ggI$55WJmpp8dyy-qvUBfcqM zDBk~Ie>Lo`W6;K%%YQC^?-Spr;O8b@>rZ*Jd3lhyE&o{Jp9Rh_cqP|rht;p;dtn3D z$AgwX6Zx|GJ9#;(a2|L&X#LBMaCz;0ho7xl{d_Wis>gdl>%S|! zeEH%{Y|FG;HO*glpYCy6@VOKH^?RW8FMB%@hvIxV5U=>#8U6zSzId`PUdaBQ*j4*p z#O|`TPxcmL_kHkV@Bs2S0F=L8QLjs+EZQsiG02Sz6DBuw?IA1uN{{kqpxu( zR2+6(hQ_D0Z}n|^0aqxs0{C*W-XZZAF1@?}@^ot?i|HGd3o<{OWAjp%9J z*_rjn#xMS4!`~sm7f<%Z3)vS>D4x*9Q^GiJ0|yYtQ!`jkXL>9GcLz@}xakR=C!EE4 zdmgBDIOKl-{uki)VbJpXD$YFlvReWFG3=^+*1mYh!WU22-Q**eelGq{{K#J}zWPUb zTG9P+4E_IqU=274wCz0}`Gw?lMaLuin_$0$I-3c;4895Wpx$-?TS3+1oE-Hie}AD} zPlL~ch5EPont`3uz%#&A?eAy!wed;5vf`P8-?`vb;5C7GLcC>-&$jm#{0fuQk#I%v zAH$F8=K$m9IIi2kP<`8Y?!m6?EAC~rCp)41HkmkWJkk>u%D3aq+8>7hk>D7EmcN+% zd<<$nIFWfE*L-j}ylcT5!1>_g;6l*)UxRr|xB=Ii&n$mWzOMl7Jf`(jasRjJ??WE; z1^a;`!5Z*5(8jNJkD&c$(7)$_mVYd~3E*GhXwoNa@dpp#)15OXY_ON)n3IXyP^1$FQM`zwCxd3c~L$Um*t;I zKb{T#oV+Q{KXI*oE>z$0(;i%q!ZLS^m!oO3-l*FVWIkJ*Sq*_rd{oU_7%#16}$P`7jI4a zPkB~cwtrSO|109pHy-h}C4S}c^gup0gO@LV4t8{(znD7KeUHtL_;;Wu{+H-2FaHwk z{uaCj%-5gvgoWzM&(rwr!u%b|N1^=3u$!-a@jgILc~)FD-z%H{265yYPvRth4l&l? z#av$wUJKgxNWaH2>er$-6U^mr0nf(W)x-aC2nKZUoV+9!J_nS3?UA6@B>6418y zbK=&~X zHy*{A?|P$kOL#T@t_Mwe5+UCG@c#&EeNE*#kiUoNk0-%rz&AkazY05tboJ}D*7?f< z>vbJ`;c;Alx{=r4U}KNHu-6Z?_J0+y--^8x!IQyJ#{PW#JO;iF7U6FXuzUsiAN)Q0 zR~~HtDPPk~zCwKQY<@%hGqJm>+An|GkncUg5|jVNK>yqP{FL~w1#brb2)+R(jQvfp z-wiA`{+{OgZ=mY$f5rc8f%dDtThOk(!DVg#ukd5@FaH|9`Q}r0eve)Cqfq@Ol-=iP zhwUHPv2_u$Cp+qQYfpMY#UWf#{5MS9Zb|*hkNQ#lRv&1;>V94H#Xl7OSa3o>U*oh0 zeT`G0#%W8Q`nmY>W9J3=lOL@QHs9^UaUpmG7~;#W=0Cf>Soc>D_4^J^ND|D+uxk{w*t2Z_W<_+mA^yD+X!%UAfLAX)Sh3^F5xAC{?qqD@#e#S z5PSjrTy{*nuW|h*SY+b;gzIlW)q{;+aR~oc;(0ibALZj6^78F+=I3|#vHc_e8gKdL zTXycnuKHQ1eih2@7xbg;KiRQ$6S5~estapRdP2n^Tv2>m*BY<#qkdFfYkXS&3Ho05pYb-Mjle*dF=+1r8r z%@5%p2o45kZQ=Dxw)D8LyT?V~XFdG&#`v-R3u>IXk`?u%1EJr>r#*fXv{Hy+LzU9x> zgY4XlUG<|-cD|+m)Nev-Pj+lw$e!%jItba5o=`lY^)H@KJYk4$>vkCRD?MAMq4A}< z-xK`OoS0yXZZ64y!KTodn`T)z*7;#VBP|CM;A z2lAtQoI<-V0GBmCN8`u#kNm4X^3AvGG-Fr&EL6V=Wp^>{u=ON6wr)c9WJh&j?MY9l zID{*TZ|izJ#-IGCAJuPn1lq50@lW#kEw~Q$q?g#mv>@b(1{0%gzkkL+qbm0i{Cs^-@bZ@%%I0snl1THkE^SHSxncmt^W<$U=! zqi6XqucSkSee;oRALGca`_!Do3ZG8L0J2}9gkKKpCC%~sbTSY6I--Ebq{A$N|+R+$j zPYL>WfNJ+|a`2@$5WV5x5nv5y^D!Oyso+`Qx&I^l1;lyK_P$<}r;Bd4E%n#p3m-%O z6|iVqZ>I{hR(3^R4DJsO4)A67JM1^Z>to_QH4v}(C!qIZ@Qi@J5bsCZ`F70#ZwD8G zyAsbnpyFFd-Ss8DL%`ACL@*S8D4w$e@n4JlW`nnLeINKBX!~b*_4D z8{iUf9r9%3m3`s*@QXmpmt674{}p-q+Z~?tFXcL4ed*ae48e}_Fpq1E_fTGJ`@V1c zHeMUwk+e(WQSH?HX8A1z@~0QbS6yiw+WFGPqj9=D^;p6<(tKfG@jeV}CZh`8#?)kAbDc-w(9yS(oqms{*TYmf0xnkzXPuaZw7Az)jruf3A?s`R5^Xz)l-ZM-(VskBSuQ|;7zXZhz9$p3kPeASi4rRGbG6C01#i~iK( zAjZ*%z<3g`625qvpRIlIG{0*eIv`+QJiC5rUXdTQ%lgwgd2EqiAKO5?E^D33wT{|# zGvsgZD)M&;`+!yLZ*$t$1KbYW9V`Zw@5Nld4}J`4U!?rod|pQT?gJkIp9lX1CXBzC zT%QCkF#b+p{XPx+U-SQXp#5s^DYW;O%h~=L@MH5Y|LVVd^C>$IVpshrRKE#j_vFBS zOLlBsgzU+V`rX!>^n{8-xT5&BZZ)3dNByX})%dagRrg&?KmUpASHQPGt1rJj_+H~! zsBtV*9CjRQJm<1+^(|k1?L1OKeDb6D+v+E%lP$oVzz|<{HGkUq-`Zc<{NA)P-+0Ec z?#|rRuW!1KJLwJ^xxW@)*ns}&;E}s{JF`J+C5e0j*an^w;LGmy*q;aQ#UFS-i^0Q) z|0vM5=PULH>(Q=I`(*zv{QMDoBoJSScc+POb>i9zwDBH~T={>Dc(*0qL%<_*#2gMgFnDe{uaSxH|2z{kgpQ`TET_pP~Hil4t(%ji;D=RGR+G<$rvR{+ms|{|dYW zyd1m=RDH0@%S~@yON(h!4j}PXycWA;SlWnA87fKD<1j( z6n-v$6X8k!1FrMcm!8eTS=dn?{=>D#e<&}uec!iz8?TM;m$XacQ|;7zXZbf4$iKBf zzUoTjQuC$8iH%3=MK$%sI4Qe$F4*j{tviTAb;}P51z)cP~%vrIP5sqc((Cb`&Qrb<=4(5Rm3Mhn!l}n5$)U;90-Q^ zva9*i&i~f_%I1%uo%zPI(U1JPws`<;{5!+z2hIgw1TFt+F6T<;F< z1KRd4uYSIMbLHm|lb;;}`N`McF|_w2?B18B{WIX53tj-u0WSt+Z*AgSAM6Tt55&K+ z?YGkI%fKtZt3lQ0*0iGv91V^Gr-Qa$WM4QPyXSzGFS+88|IPCB_e<0)t8>l z!+h*24@KD3cn#&nw(tA4Z{xM`Jw!V-{?ty*PnN%^K>k|=@>N%MTx$H=cqCs)|7c#= zH88&Z2JcN!yuAW^@iae+7n+~Nv-7yt8yo)@`27}K^HkOe(DFmP68JUXB(McM0krnS zn__sU1^DgUSO0YnAE(Y~r_HyP{q z@}H~zZY9pU!27|6!N2~?<2|oS>V~=Z17j0jaT-C zS73KOX!(*W9{C@Vr@sfVYxT>~uK@G4Cp$JTui{5}ITE{?4?=me?ft&(+jwn!@6%3= zSG7~~pXIMj9`fa{Um#y~ruo5+e;bc@8poP9zG1vszUCRt=NmB&HGf;a)+f#PYr&VE z<%?(6Gp*b5qjp*S?yRGSgA2jW0_&^z+w;9}=Kz0ikHWb2lzht1!(!QI~x1V*qvMK z?LP$`L3?XK8~;D3vwwldl8?z?C_nOd0)BrCo)KtYi1%X???%M=18_u+_|=cs5dZVw z>cpRKdqVM@9ca(>$Zs<^pX&#}M?u?v%d4NS-(2}U$mF+yYvps^GUhYi_&y~+eekPz za{0IUT!;SN2;2n&x#n+;v%%#1A;$TWpp9QV&Bx+tzO{VCVb>eY*P2)E3iy*BtzYt^^~(CwI(Q}P zrPf)kQ@PefyH14c|7s=KpU67CvhD9f{7qn+Y0t6D$MdK^?IUbH6tA$E_*=mc-|~I~ z{~GWv@GaRf@ohqU-N14a?_I?G5cnvVZ#;@$IGs3708dd|#$YH9vLnpbp7_F$-uIP1 z#U;!)zU#@K?O*lhmh|Ty%h|s_!H><4{Hwn6tryw30=ueHq3Tj7yY~m`Pj+nGhwRCY zt?SVElpUes5}NW&h$j?J7~MJl{I+*_yMNd4^gVeBweJPu_!p@B-W|B#`Jh1lEbN>Io)2CM-U$8ywC(u{f8FT6 zKf-$oR6ks)_8J2lptr30ld#(YP6bZ|F90tBjr+tN`0oiSPW6-J--w<0;N9S(;EUiQ z(CW)isQ&+dU1)sU_7=+5ydnIMx?4Mt|Jn3&uik#X(zx5?9vg|jeg^*g;M_8= z_cCa;u10<%cn|n!fG@k7mU_D-Jw1-=jK*oif>4uJ(H2Q8JxlOsoa{0IU{0(tl1zrcv18)Ui16Q>E6SU)V@ZaEn zK#j*=($2Zy9iZx6d9d-xzHlLS-vBLNa>XNmH|6Q?ee6pATJ-YOm!8eThS&+^;c>pV z?OE0K?jep)JU#ec<4ZivUpD?;1@ik9$hYG+*Zi$t%DO-FSX8UoyxT?+I25v|EQH@e;3yAm2Lkc{Om(J zdYSfY!?JNpyIAaG^dlYgP|Y~%R8@~60j`Nr2s{%rrMKi{EWzgo`z9f}{DANg1P z=UXqbGXcA*Q=#foD7&Ww>Q8oT-G}VSj;-s^_>~=@;u6~W6;CLhFvPe0_#XL@p88ee zbj!egM&ta?=>G$J2bBK00e#JTpTXC>C)B*RL!SD%`0``t*>dVuezZQ>{3YOR0d5b< zp5@D~)(@?Bs^eA7uOQyM<6$3C-p}8^>b`UJdu`vWS5!P9`=NN0r!C0GB9j-J2iu22J?L`zT){8aaHy4{dpE>?H_^s|G)-tdVnvxTl4)KP<4^3{o-Ga-u2*(pp7rY z`&D1xuA9Ke!PmgusM8Wq@eQOtuLti29|m6pRmb_pb88^}r;xv7@D;A#2Hyj%zvb1> z*Ke+Tb|;U@-&xC;zkK7_f`06Z-;n=Y{dEX&jsQo4M}uR*c2M@-;M(@bindowJ4b^@ zfX9HU?+3a55c~xE0_;MbY`n5BEQ8-4w0y}GkNn@2r@uqtN&hyk^VOH0&BH|OC=U;D zt??epi*4WcZQsUg<7=f|8jotH<~Pefr$GMs1@cu_8i#hiwDD-17EzB^Fpf0eTE2Li zuQjin9~ggjKEDW_{8;<4D?P=bc38gFwe6XYcQAg?eEt|?ZXbc&oceB_`;F&+ql4dasu*e!3E&o0{k-y%aw|+HK>1l<%JeUk2X+Enjxe=6mJw z7Bk+abANUw_)6e@P5PVCA6tXl1^Oq%>xTcmz$$POcpa$upvlB{eIUM&znuc{4MaZF zU=`O#fl1KjcX{>m^_weS7npoi=9#a2{hv+ye~KUL@1Q*Wtpjf(a5Jz6xD6o^vA^YMB#TVN64(*1>s<5{$HE-^Cit!3380PVWaP4c1$IOBe{A?Sza4dK)sP*Iqli$ifK12R?3dGkRd8NT&Tptb| z1KNBnuYSIMbLHz?ldqrTnXi2PpGo_til?Pi7vM;<6ySIXtFS+88|Go0`cMo=@{{!^$)t8>l!%Nsz z9?G$6^I+Svs_pGdT#C=e^A_#U_z_R@jpct^Ab$<~=WAc%M&niEM&s4SBi^O>xtQ^B zC20BL{T{w}nm;XHJk7U?$$v`X;P1^ngZvfnZSdUyKg4_fG`~Ms z0&e(Ye_aHg13m+m&GO&(0sDi8ffGQ}yhO*(*GCN`RYs0=HV*r zC=Xp|x8{dXUTpimZ~Hc08{f^eQ{!3f)O=|9e=d-}ut2`*>R9r9A~*}Q@rb8!E}rIZ z%hx>Af<4Vcn%^xy(cjPewF5m)THqN^KG2C73%bD3aY)E1=<_(H$Kq5Lix90 z=N|9@@R5N15bv)6zT_|P{mbBM0e*=0UVtz8Cw%_}_)UNx;;m2Jerf90){pGw%U=z9 z`SQiv9=*jTE*no*zR#Dxd4YV{wc}fH*?4xwe!l$O3*_5zq;a!tVEkyDY5v(2UTA*F zH9yIZ?AZ3J-RIJ-$3QzTZpir8ym!t5Z|Qna^MbWM5PPQw?91M%dD@$Roks9@a2ja+ zpMpH(|G%9-oxCVN?{gi>_w9UtKlmW{B={=$4wx_>t%K@s`Bi@#dO~$`3wi!Ec7-AS z`S7m+e-B#zfzm z_t)pap95YD&IPTO{WPk{gThiuFJ zwfMrH(7toPUx9m7czg4}KVbhp@JZuu34S;KqxbU%@B#47Ks@*0?>SKZ7US2p;tu5Z zflq)h1o*Q1HTGq<(Dn?b9kWdPY<#l+3GsXlejA7{#QSKF&wp3)yf=6VsQA_2ubccU zUK?M?-x~O{{2h?*Zg6j|_XYO@SG0bc%5VK?k*{D$&k+xLCjxAEHe{!BYH{?ty*SC;=~ zf&6z1oY;6YPUq4;nin2pd|AGD*Ta7b-roXz@ibqHr}^6253R3up4WP7 z^?S3Lt{vQkED-Pvfair?aA%a<>n;*s4@Jpaai zzWi+%cVod4##uS2@gp?NPZXiI3s?-6g8PCY`?>5%UugNu>u-30@r^2wuXdk+p5`;< z*|z7ujo(b%`Nm)9d~Dmly!wYy|6{?&i2p^<`diujQ3b|x6ywg~SmZYT5U)BD}FXpg+N`d@Y1@bR|e+#JgJ{f556$SFIDUh!?luyN>JlXyh z|1bE-#s4F`eEF-9hl4<^Gs=_pRYDuzTIj6@ZU$}vZViU)=dve#q2({Hzg`8#*RMdn z#?^c1DUWXl?&k{SU&K6jE7+C(*Scf#_20(-eSz^SZ`z+|AAd|>KVK-ni2I9=kMQ>$ zx^Js{)MnCOH^4sutU+%wc;O?ScPD7ANKbh4DDU@nFvNcWe+#QTzx*(di@?N4e{J=D zh2B-5_#dFR1pHEVjDfjE|6O?b>dS8^{-B*edt`6E$;UV3MePup<|K0ITmJ9yBY!XA zNB%5dJhfLm^;ez7EHMmj5BV zeEH(7Gd$Do8Q7l(E&vyT--5qpTs{gu!MJ=2{2C06_tC?=-}gY-e-Zoh0`{NJ(|#>> zr-3Jcr+_wpe?=aOXI1lGEHIv2`LgjRn1_qOzF-A76b$vB@)W8c^`q*=`d?oC|BxT~ zpHAJVem^qxYW+*E2ld}R+SlVeP^`}j`Y1(j57yx_ zaDVV%a5#7rX!q^2kZYgtJlF4n_Ws~z?ms>ND_HMMxg-?-4ealS5N|#BPjHtU{&ZjR zRStiDB(D#He*$y)->^Xc-2&|m`G1i(Y<>#mZ$%z=1NQ_=Kx;q5+b_UB1bG!W20S{z z5AkXOe90&CeKR;Uzz^|e!B3icvhA1MeEBC}H($PZbI=<@Jc`T4^AmXa^3N-fFT1wh z6_<_YQtao;zq~-c#?|JGFO8e+0^>)#ZQ+ZzTYxW~<`3~g^U=M;{}K2Z_$Bxam|%XL zMt}YYyajw5wE6re`uW;l(f0nZg5$ey1>3XvdV_o^Z#LhSpKo5f(|@+#ti4dbuc`f) zQCJtb?4M13)Q^Uq*vRmIk%KS0d!u(Ce65?-zIb*&AU&<8{=|1Uh2eL(}*0-hS!FI-Dp z>)~%U@yVa;3T?bMqQ4N7{#@)>{sRT_A1RQpI6gpco@tMb=M8xI^2JlW)K2-a_7mha zU;Z}m4>A2;8_1{l!X41-1=@J}8T*yk4cR}?@JHt0EACqKWLI@z;|cNW@gvNoKLUS8 z$}iZCoshoHDRthYa|f;aHs0aPNBQzc709>e6px#81shM|NWV_@uJt&9^Y7+Abz;VX zZt#UYxZWBp0*gUwMe^N__5K$ApT|Rz9>;-mz{EH&FA2ZI(KJ7mM939Aq+EYt=-^2bVpzKY@uFZ$^t~koa-F}S6d0;61 z*NvaAxc&~@#k6lK?X&(bCGN|>tHE6H+*F|d;RX7?ojCH1H^l!AI~$VUuHfdNwIAYb z1%G>R*MNSAR}$b$-k0yo!9xQ45N~vVFZr>2Ukf$__#xhj@aLNPwE2_WeEG*?H($PZ zKSOUe@hC1EPdmJP`KK4imt8wv6qk+X7ue62|LX$z8kd^yH13WGj6cnXe`7q0Hz~ju zPxFs>q50)%;=c~O3A_!o`MDc;!_j^oY^Hzhyx4{H!_Iq}XMfCmdlvWvm}@?l|DEu^ z8@SgBiYL_G$7uHx;P>tS*+BnDzN+GTg*e^=zXZPy#24Z%YrM9XiHisEk_ zs5klf6u$|^fyRUNzXf(yls(xA$KY2uk!#I=man)ppDC`;{#15^&Dfs`TKgg1iSV^w zU&OxM>WgP2y>Ues^8-&CGL_nXRF=zi1YNB;MqUHSUoF>t?IsQ)eSyEV8osCW|8?ONbQpz<@5 z>yyDVLFH>B;@k|}1`Ne-@6T<%6qoudWe2+I;*4+MvTM+Eq?+m8K2wZG3OVLq$^o58<f;!W=k+Z&31>p*+^A@6Ung6rYnDA49>dG+)4yR7-oH{aJUW4`l^{}l3P>s|dV|GDZ} z{40oeYwCYTa940|uml_h+WuS7_AaKK4}*_^Pk@>)CYtfmh4HbW89z2&*%w}h-CID* zmt674e-i(>{N0UR>5oA#Uw!G>JiLru<-yLM@8HMg#kTMJwy*hK@!I$n(@u?F@ibpr z{_3LKLJk9^szIa+s#nXCe z`C7N%8td2XU$D+E0ktn*NFFaBZsn;P>#gz>vj5UbvVT7Nj+Je{nfT{{^G$nRAz#;! zC*{lLL-GECxUU3n(EiCNgm}-u|2z0GsQ$ZzcrF8PH}TyO!bE{}AYZ*|Bvdd)qJ$ z%0bx`s_qt0fAVANE;L@`M<}~OTYusS#S@13wjGm9do+&Jj^}8PwWmBBivC!z4wU|5 z0e#IgZSXbE2sO{VmZyF$zWmsEZ2|t}N9%!YZ#%pT!OK9|vwYdre696G<7QR!A12;> z<0<9*tO6Vg9tDoz{9#n!yh3`yLiNWiqd)7X4LdWyQ$U@Ys2w^dID>lAdm>fL7tNr~ zL2dsjFV)zWJ)z}`r})G>aT)y|hhNz{9y`lwPj+nHX5&Zwu5)bVP3v-M90l%2zHj@v z;?udsPV57B1NR1Pe~Pz@;q4dTU&nq@_qnBk_l836Q{9CA-QayW>>Y*u34!NPPoV!- z@M&;$?w4(Rw*4XfzhTGnCI1)S=dwSadFCZh>!|L#to^?h$d}z$(0d!ydt}!BPw7|P zC#*?3be~}PdavSs^mL9nH;|w9S^hrdr{E>vLU7*c{`&{OL1*~udeHip-V*es_Z<3` zFWxD9e+?+V(v#nizU+uE4Dr7pKJi`-#3x=I-;1}H>ov$juRuP^;T;Md3(8&*dOLx8 zg8Qr@dx}e_xP*#BX!Cg-?GZL|eLUC(hWht?zE>Xm((X`y%a5>7e>VQ|T3^q**Ll=D z*dCa_M#Db>oC_`lt^JwEF9f^dX9qCEm)?)iJ2i)W%d`GA!{3%U{Hqbz zdv}7j_t$!lo0;~?o{fJU^wtG80%h-=fW0|^dJWmz2D|zC5AiR>&Q;)b;7tMhA>RD~ zzVi11?Y@C_hVm!gtMD&}FFo5{@swBPQF@mD9dULy^>SpO9(P4v4DJsO4)A67K>>r1pR&ZJ%z7VgPe9r_g1#bs!{QJ|6!Fjf42ij2z zo|9+$Lh+s$Xy31q|JLADT;B-Z0^0gmUj2Oi=9}+)^ZPAzQ)qtkjrSkqOYw}P-&L=< z{J&3s+kEef`~YwOcnCNIJPBOU`d`qlZK?AjaA#2C;T*0t9`5A&K2YPs#wYv2ukp7Y zdX_J_;*tMZdHUN7p7c-VI$wS1**uhBM|qgTwatTV&#Ja}9&sr?8_)iHukk0|rN}M+ z@B;bA6v)>&3atkkH=+4o>&Xuok6jrjJp$uJyi)k$X@0it7fYE&1}%Rs-~SnWlX*`V;$H>-R`4Fs^2NK(@Irj?ZZ!Ny0`}iVE_{#c4|4D= zFT~gWR{5~|2;1KBC%i=1pX>SHo#1_7zWV=HAph_Hzx^-XqsB+k#r|63VH|Z_4K{Rci@-t0qYu`A&ERZ<4{*H@{1QyG zcsr$FIe0wS4n6=b0^d8)>n#DVJ;`6s178D|fES$Vzn=?EnB}i^KBn_7)p@9%ZGG$d zoov9gc{B2^$Y(iREzu*r1zNf+c7 zIE?HsfIl7i-N?)T>IDOlzkqx;@;#A%j(h>~4Uuo>8W7oEh`_^nym@GmtkM z`Ax_dAs-F@W#r{gdBMKOH+0R6{AxU}hkPLNcEdj&`5uga@vlU_0Dgb$+=smD(_SF? zbI8XT`G?5oA@6~mqBY15@(qv=aq_6Yx|sf&2!9d$UEt3}-i!F#kZ-i6x8L;{FQ`Gj zAM$J9Z-aa?@`sSmL4GgtMc6qD`QMPYKkEhOBTua5{a${l7mP(d5qZ&bUT_TZXOVvg z|6JtXA)oiW7o3B9*R{R5lw1_@?ppqBGM#1di+lKCOwXkZ*Q{mlwhR!TR1#)xW&pRQwJ>-V^@G_?_zHQGd0s z~emeGlh^GQ{r4Cepm1JOyvJWKG*QK z-^j;#3-Vs@hai6&`5wp{k$1h#3$*TBi+m7r#s4(&!;$ZXo&Fnpzb7LvLVhap1>5-e zPoce+IQiO%O<+Yi^q*^xyWtvtgnz`&Z?OLj-@k$UqT9XTW8~i=AIAVvU2W|e67}QR zf5_w?fV_QIAIS*n?HJ@I-QoFPVgF?03-|E+E2-PxA)hwi6Di-sGssW4)60KJgWo|u zyCl=jwk|bMoEzWm`K!Y}5_x%V1o&-5zR5kFuRQ!5`Rez2`3ThRM80r;FHk+ag8Z=i zJYVbS+O8m@coGMA{wL(84EYrJUtzxn`J@Hjj*h)cH}iI$e89`E_mDUdx&D2|YU<>7 z$o21S&~1sQkn8WKK7-#ayLtQieUJ5svl6+Ud%3j7pI;%@^P0WgIvss(N3Qc`w$F(d zk!yc^vZp6LbMk1OOswq#FDIV0H;?2SCKg=j1vT)uLp~1qQOJ8CPaq$P{7~fWS9m*< zkxvpIc_Z?liGR80OMW}@qThJ=80zO;@h|oAUt?#ZEqwe5!B38Ssxm zz5scVHxJf%3*j&7l9|tIWBG=O_N%?&+0@DX$jh(t@_xvl zb@FWYBX2l)RL}c7;RA8YW&Bxx%k2CuoIK($fDb*<9r+R?-xGPsU&Qp--^ru(qKNjA zp2VBUU@k(X@w`Mu*O5*sCEUzf>W z5ZiIZ&blA5qR(GqdG|zj+NF8>T_;~Xu|ZPq@v| zZ0}G19TM|5Pb~R5)Bb4eD9_@LL;huSW92@I^9uD=%-t@ftn6yo7a7>-d3AzN6#oJmCk_?S05o=N#C11-Z^Qc0sYbU#G#=-LJf0T{)oILVd z(#=agVqV>3YcHQoJs078gp+5BXKX;;gq`*$y@CC(b5DT(f|Ez>s^YxGEg$jcODEqU z(Jj$VJ-hCUKi%BImo1)MoIJ9l^CsCD8{kiN^2q*@FMS>~zx@O|I#-jOYmw_bYBv)4 zMZo^rt{|g$bp9qgJ2-i^_>1A|e5jQ6p629He;s>?7mP!GC32l3_C$WClSl0;XWY$0 z{*set^ZN;Q%KP~S&0<~NY&##n&fDB&{Mpa_5XIB}roSG8ywSf=3=Ml zRd3*E{I0ou_I}*o$+Ow7baL&<2_=TQSnlF{(M#R57Jq)~Z+G%d-1;(rd5rQ&{N2gpe7@n0FdP&ALB8m9FW00X>@b@FWSob2SAxcPkC27Y3jvzk{p&&lI>p7Yn=V&^vG z3yDW@J{!v+rNOsOp3UzDJ7$+}>*U$`aX%-I#*xmwSHnk3%y;AceJ?l+`MGYQi2Aq6 z%o`U6{9YH3-;4cb>Rf)`b8`2;PM@`Q${v4rCy(a=&Vlblb7IWzydItI&V4E63vJAV_)ac{o${*bM`#%7LX4N$VUX^^vHfKQB9Pc>N#HuCD|9&)ooz$F=GA^>)dghwYs_;&=bIk5lvi;Q{_+C(qX2U&eAb z&pht~bm7IHy8?Fp8nE*kcB=m2^&iL10N3!Se(bt%p_6ADU)MN!G=9t9^#+Qt^RVRW z`t}}*e9K+4+bMSP$j-vQd*Z`hEio2(`@vo?d=3BokC89pJmgU1w>WvU|5-%cy5%zd zJRQs3ITiD;>Ujxv=F#tJA_b+cw0X=}sQy&5nzEv7_g5 zgqirz$>j?ZF|5A3_nYYN+grQ3|Mt(ZJe}!(e#OZnzX_fLX&v9k-NP9C*a&tETJ)636-RnZ`LVIwjYsOA;3nY4GJlSljB1)O(j-}__uC5t`r zariemc|3kuU)=Fj{COPy+^v0try}p^Zp5N?mGeG?#!;1%M{zD99_4?!lV|JSvjXzD zP9F8wBKk|ZcVfTm`#z9OiRZ;w&P5vh$H}8QJj9%{@#h8EVZ3X8SYzMpe!Drj`T`R%Obqa+I(gJDRc3y; z$jPI4^nS=A>_fKelimJ_P9E2xSvQ||@@)Be8@}Gh8APy~^o`=_{HbvAY<7-u^0?05 z^Ez9iI@QUe{vAjEZjSss_*G`!dkMK+N4|FQY<@R(3r)5-cXaZ&erEZ~d6xoeck*m@ zW;=O2pP2dl-dNr_4{tkpG#=-e`EzY|@RiMeHz$wm+x5Cf-qhXr#PKulDYyGLd1R;AocGo_dA4><4am=R z@+khfW}kU2_U%0UYQWA{0r~pwh9z4(TL&6^ zB;-ZROLrl^FqU(X1~&)n-y7h+;^a~O^?s!K@!MFQW|$whKgj2~i`fq!@QB9 ztzGY7$Bv_-0Y094F7bB8;rBErkLJ&EvtD24UpBzbl330~8f-Gqw`-gk z*MpqAD2=Bd$HTYh_CG;x&ru(6@@)H)7scnfjN6vSpPdi(d6>;U)`cB^jz*sP_flxs znaJn9?s;p$zZ!Y_2VU?3@|R*c7iqAUE2O9%n&0q3?em8rFJfG{CRZ{SGm?CIoDJ{MAlyCFXje)}_CFb4VUv7C!EcnrS1e|g8rqj+YR^=>tHgODws zJ2`p8pJ(=a{hd6n2eZB$gpOB>e@=n#!L`dwODPU0ZFvvyf)E!)k1? z)DvqO>Z)7PXzAfitZR$oH(!^b>b{QJS2H=;>Y7oT#;TeYn;{m*v$4eA_oPu%8(XK> zx7AFl9^X)xbb(K3YN@GfZD>w6m>GOoA0K>H&84YDxvFc79W^z#)i*U(HzcDBG)`?! zWqnjhobs&Exu#@`F5H}uLUBz~Lv56#tWA+tdQ)N_`Ln%)iWzNnEse>lnS&|Jxl0DS(J2le)z&AOteG*Rw6wHre06JmjjPudr&U=qs;#utRa{HdhWfVF z%9@H%)$TtPrDe%s$(p7{$D3NyR$1jl(?%yp^+}FuD=TqbKfSu8Hd*hITHV&_UqX)wXtIOsAR8} zx(ThNeFikOv@}hxs~uT4p{}K_v8Jw4qxs0{+M!W5l_pC|hc}IA9yqD4W^!Fig`4=? zC)M_q>?UJw?r4*bzPCnBM{J5~Xla?BhFqFli_-_I8puSL2d~3u@08KzYL~91&Efe` zJDGOVOJ~!5$|h3BFfGdUb%zsHhx*p!#F|vWB+{p#y0)sgWm$zChFBiz8|$;?U{qN& zPrJo5H?Lyf$0g$Ms-bF=X&U%4=wFEF*&KavDPhU zbu;SQDn<@T4ToMMrZ+BaER!_ktKbWUsC zMk{^r`BGiV_Z>zwFysA4x(y9rUt}F4eQ88*Hoeeec2sm_CH1ayTB7ZS58KVEP18FP z!%UgfRH+P9cI@9YSGxMhGABjz<;dYKou#E!Gb>tJs_N_99x7h8zc1e%S!PCv+ng=Q z@vzKA>M++YPNCa$B;6V{)eW)Qx(Ts~J~d6vGm}khG1_J}*Ez9E{=O-NR4+uUTQspu zt0?WAzCG$+Rhd334X>M?emS_VZQ#_FXi`p>j*j}$2`ZhvWNLfUsTyLMTG7$> zpP!rxGrO-}KUUZx?{@pi7Po#U?yM3^ zQ}nrx7}5PnXS=~~(iAmsu)9Nh#>t#L!HwPTLTq7$tPX*$`s(Ex6n z)YKUFuj}~Kl$XU^R2LmHgfEY@DK<-Wq@vGM%~h6}?9^J;r}Kp6_Hvr3dIo2zZ0^!m znoM_9&R_QObn`NI2CiIc>N<8C$z)rD+kZr3CoUh?Rq16D-!E4;#FIvL3qHcMxx8cO zD|ol=;k?Gr32E-5Tl~P69qO@Xw|C5p-EXaFS+;Le2K+qeZoH;fm6j%>>5gLqvv}ph z>(pL1^Q9b>u}o&%Zs&AcxK_%c_Gm42&7ICB*zK9Gq?&djlPYz$Eck6(YA6jY^-Dsu zd*I8eAsqvWU3(6WT&9e<&XTb-Tk4EVqq(>;o@LONx?*;RNeyw?6tc6-ewP)^8t$7u zl~blPwYZz~ifL|n8}7DovFYfW&V#(8c^d1E*J)QWs+YU_tf&|@Xz6=A&4|efZb5FH z6uT`+mX)N=p&Ez5C}SiRz~TmbHPCf8|_bb6LX z0R&DCQ>4_H&Qi|DL}pW(%$QSMP%Z^NsodLFwP8nag$TCLDQx& z9&1`$i>FSQP+XI6w`+A1-L1A8Ep1bx47+o|MACU=%}v(2OE9l=?d)U;vsdWSWnSMp*t)p9z4lb=Q#}Z{#GY3^iYhRSp zgGRsxtY#eRcMa?gHbScj~5!gWU~ChEggSoo;0!TF3LS4ql(}b#4}z z*y<+br7T{qX~XKKBuDyt(x{;=Ey)3{M_VRNof6$}r&nUic+X|Ky>;eC#823ggX`M* zyZia-R1Pxyl=d3X(log)bpl4^q|X4QX-TihwCh&53~Q;VbJS5(o~Zx_x}Bc0GWziJ zs!A_cQ&pKGzf<@18|`ir;?i*=bQvBhIB!)mlcQUv){TmvkYqHl!+4yis+pswx(Pm6 zc62kAu*_fon8|smqRn%iIUXk04y-x~D(Y@ou?e;j-B@eH?XSRC~jlTBf)q z;DkCi;xt)jE%1)pi!6)PR##MHiGTKV9il>-JId1SI@aj&S2p)S@LR5U+j43A#@ z-N>sSKh-^W_iJSO)R9>xvXRQoGC!;`y^--UKxtcZ>QTSjHf4FhzYHU>pL zjGpjKNzYnwS!u@v63uWfA6aWA{m!YMn`rzpU}h9EK584>jmuJ6oBaecOWmif>Ycvz zg_?ep&5RPQ5rf>pe^fzwq#aFZ83B3iPWPir4Lj8=_hdMlJlg7~xMw#}{X|=tAvK-z z8%+`;{h6hUCJG{1**a?K_~;;piD$gq3OBmx$Bq7SSJC|kR3=M{duGI1nspI2InT0Z zNsYEF4@q1&2fLR)lB4R{D%*)y|EqCU=LG z-g~9JQfps)8r{-bQ57#nQLMw=ogI4sXTt4DGiTi>_i1~URJksF?-MP)>7|>RwW8NA z+|6h7q$TMJZerUcKRC_EjKWV}FVd%B&GpghqaKO+Vo=7c#<`Kd`7PGUYeJ(C-NJ|s4skH#>-!Zm-Gj)?vQY?Cs8D}(OI>@q5$k^F0?dHvl zjNb8UbUvS<`YX-zM#w~&xuK8dwB?<*I&GahQdbtg-Ik1>jKw=WKgwLgSm6fOxpUC^ zn*PyVA)}L0HvR{XrxWo(U~uebADW%dz=Kcbxou3J!&+y$BeE$zyY=3)-|-fgMte72 zG)cx=8$UxcO{JH-IbQDkwnRn`We%fUkJzHYa(L#UQS>fdLv^dWk&pH|^^FsnDl%6h zx1RP(P0~#h-RpMo2CCB>9WQGW-1aT|l9qm}lV!WoahK`ZunZgLcnEdef0?aTd~nk2 z7Mkc8^AvXvF~c3vx?-OkEDtlM#xJ#GBtHN8S?ZjacGuexea0MBn#YYshick`Vz-qd zY`3txbF_GLyDE%!A<t>>i%G8@0{{bx{GP zAElXoigpUo&A>1P8*Rr^+m&I_mODCl&e()xI~MK8N5*uz^u0YaX zXmvv(8NIMjJ<+|W6it}XGwA`*);1o->1ir8YftQMO5K#2=9#HFEA8cMRJxZSTH2Bx z{4PGDwla#(-Kb0(>7O5`ZC^7_20AVJW+m(E?p4zkvs8Mr7qK30>aaHJ%WJ+8{9e|L zr~1rYmVIv~YF@_@np#%MN?eCVZ&~bs+B2hg<4iVA&p1cOtTdBb zwvU-RrgR5U(`y^5rcFs(B#Qe*Hap#7N9CWs2xd4=l|X6FPB*6MPbV^_!cK?y+3UBE zMcj@YZ_%8Tx$L`>t%~?p3^H1gxmGt$ub8rwuhR!%$3e%K^xu={Jf)=H8<&>U*4OHR zI`?cD(`kMcnz>2nI3(-*MnGP#{baT{?QT5FY@6Pier-+7{xl-0(&*l^ZhGov@@SHb z?yH+y+@nNy%oV*nc8FWNowM}XOjXg+Mx?5ybAh_f4;E;~yGdoGSsp7-tV!M2#<%+6 zvW?F)lC?7%tEaf_Q%!YiTiWQ!%wHa}M4VSYAGoui)XjAIx|+&Mt{d}=*Y+rO^&%dZQLmo}*dHV`iOzy=~cyXj*#S`GaE(jWK@sCM|Xt7N$p;v!{Kw#*S2b!yti&hFczX`LwIrxF>d%a{-2O>ouB zC}${iUcv26LA2xWsgAgASJxDHpzL1c(Z)D>S0&d=bQ$$!4|zFOPrHFgpJqz?PIONw z-K#RLuiaZRNw-ebMM;PPj2_h;cG^R1SahvR9qw0>I8vVXne2~upeM75Gy)*jSEF|$V zp0>*SNh)x3>pZ32(};d>*U=8%P4^w6%M^F|K0EiVXy#pTYC?(nEpik;Qy$akei2g7 zV>>#(W1pC1`pJCgZ04Y};{|Q+I(7ot_Ydhe*z6xt9aGsFy>t+DLgw=p96gIX#~|Y|EUPE21rC$0HM-r^W9o)s;mZ=w5k?p0|x^sIRF@@6hNOQ2gFa`n=ug zEiT^?dS5GJe5Sf9{iYzjNhyL)b9|re9<6nL?C71u?;^PI)A>cW^kJ0lz)pI;5|SEe zlj~+CYusGjR%a&v%!xDeO&U`yt9Fyp>1ZmI!pyy5=U=GE=$TG$2l+e1jE>Hj|1ulh z;WXp7I5J#tj~Vxz|CKHudumujPft^)asGK@M%QKB6nDDoOofnZYwhYK{)GxR_gB}} zcC7L}U8yJCQ{}X0xf$`N94y1@%*^qfX84TurQbwygPyi~H)n=rOMAS*0+60iCyHKk z$!JmMr@_&CF&Syj^0Ypau!4R+Ix{ZQ*7W-UJyUg4QKXZWpp3Z7_MPtDHg@kmG`d@v z=rGg0KVGX>_S}v>o>@4xb)!8x$m@8ip}0?GI9cb}^qqvVR5{w+{G?tQcCWv>mj&xa z^I}lPLgYt+v+7o?wAZQZLYuvNOm0|Pl4BaDM~ArXB@XwBVfo;J1Kle}(QoowuX((w zW{!bUuLq>r%J{{u(q7SM%oQX1xutDj=Vzsv=SH1>g&-r`%rZ;8(vsHU zX{&N+@A%gxC%8x8(W_=PlilvVYRI(sT&nPUwCHstzY0{0oRV>hXm8l;wApD^vV_iW zn)*$^n8s!ob^O*vW)?CxW6^C(`fZvkA@_vb-Q~FlaITm_d+JE7pBPOgxy0?w``%cu&8TcZ)~I zw%c#IT`M}>mPI|$`2{SugP9V&iV?kTAHU!qz3JKPUcE?8t8ZzWTHTN~IYr4y8w8y@ z%gt`lJfvS;Ftbqli!7N#EOSNA7~vh;wajy=?B^9K(4{^|={<00h5HK~@!?9SIXNcr z3Dct&NXvVPHjMlhkZv=cTr8{M3ogbamrI_l%Uw=y|?= zt{y+WrEZ$8;$Oms%ur_ zCyL@<%l%*RdsZS*%XhL@bgchI*moO$E&tM2`y0_8;Ax2?XZRn4llUWRk$&_yhpm1@ zk8-lM`+xkIor(IpqDbR*=;sJq-T%||i_jOwotvhhzxgIS+3H{H$@=%Br|@^RWuizw zfU_K?``5qQCUk!(HscfZL;B~Z?jq7x{u8sKEB66sZ{;ub-y;10Uf?iYU;qA`uov|& zHS2$_(SPh_?_YT352Wc){U_;lM!(@jub;xr#Pn$W-{ZuQ%{BN!|305k{Uj^a|AX#} zjUs&r(i4XCWheUcqZ_B`>)#U;iii1B{Yw5f_eIoV`A_{jfBYT7C8oheG6cTkFsgm& z>)+)QcBL?6Mt;?32-r&xa$=Ezl^^AeLmsrXwpdg$iMJg_g%Vw{kzA) zxguhtD9u*%AK|9%hx8#* zw2x1?{?2=mh+eJJ{Ii?;-cVv@GylGEdum{N1D4w-puc1juQ)dP2hY6ZO76Ci!zB=j?*bnDXI z{twme+;8daZw=_5HNpQC)voW$llPE$<@1plq36x{e+z<~{CPkZ!dC%O3xODxoMH7J o$d|CR3sDInBN`z3Cj!qCMF!~hgU&L=rvK$hi2g=24QN~j08x=Fxc~qF literal 0 HcmV?d00001 diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet.cpp b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet.cpp new file mode 100644 index 0000000..e8d92a0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet.cpp @@ -0,0 +1,320 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +/* Format with: + * clang-format -i --style=file src/greenlet/greenlet.c + * + * + * Fix missing braces with: + * clang-tidy src/greenlet/greenlet.c -fix -checks="readability-braces-around-statements" +*/ +#include +#include +#include +#include + + +#define PY_SSIZE_T_CLEAN +#include +#include "structmember.h" // PyMemberDef + +#include "greenlet_internal.hpp" +// Code after this point can assume access to things declared in stdint.h, +// including the fixed-width types. This goes for the platform-specific switch functions +// as well. +#include "greenlet_refs.hpp" +#include "greenlet_slp_switch.hpp" + +#include "greenlet_thread_support.hpp" +#include "TGreenlet.hpp" + +#include "TGreenletGlobals.cpp" + +#include "TGreenlet.cpp" +#include "TMainGreenlet.cpp" +#include "TUserGreenlet.cpp" +#include "TBrokenGreenlet.cpp" +#include "TExceptionState.cpp" +#include "TPythonState.cpp" +#include "TStackState.cpp" + +#include "TThreadState.hpp" +#include "TThreadStateCreator.hpp" +#include "TThreadStateDestroy.cpp" + +#include "PyGreenlet.cpp" +#include "PyGreenletUnswitchable.cpp" +#include "CObjects.cpp" + +using greenlet::LockGuard; +using greenlet::LockInitError; +using greenlet::PyErrOccurred; +using greenlet::Require; + +using greenlet::g_handle_exit; +using greenlet::single_result; + +using greenlet::Greenlet; +using greenlet::UserGreenlet; +using greenlet::MainGreenlet; +using greenlet::BrokenGreenlet; +using greenlet::ThreadState; +using greenlet::PythonState; + + + +// ******* Implementation of things from included files +template +greenlet::refs::_BorrowedGreenlet& greenlet::refs::_BorrowedGreenlet::operator=(const greenlet::refs::BorrowedObject& other) +{ + this->_set_raw_pointer(static_cast(other)); + return *this; +} + +template +inline greenlet::refs::_BorrowedGreenlet::operator Greenlet*() const noexcept +{ + if (!this->p) { + return nullptr; + } + return reinterpret_cast(this->p)->pimpl; +} + +template +greenlet::refs::_BorrowedGreenlet::_BorrowedGreenlet(const BorrowedObject& p) + : BorrowedReference(nullptr) +{ + + this->_set_raw_pointer(p.borrow()); +} + +template +inline greenlet::refs::_OwnedGreenlet::operator Greenlet*() const noexcept +{ + if (!this->p) { + return nullptr; + } + return reinterpret_cast(this->p)->pimpl; +} + + + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wmissing-field-initializers" +# pragma clang diagnostic ignored "-Wwritable-strings" +#elif defined(__GNUC__) +# pragma GCC diagnostic push +// warning: ISO C++ forbids converting a string constant to ‘char*’ +// (The python APIs aren't const correct and accept writable char*) +# pragma GCC diagnostic ignored "-Wwrite-strings" +#endif + + +/*********************************************************** + +A PyGreenlet is a range of C stack addresses that must be +saved and restored in such a way that the full range of the +stack contains valid data when we switch to it. + +Stack layout for a greenlet: + + | ^^^ | + | older data | + | | + stack_stop . |_______________| + . | | + . | greenlet data | + . | in stack | + . * |_______________| . . _____________ stack_copy + stack_saved + . | | | | + . | data | |greenlet data| + . | unrelated | | saved | + . | to | | in heap | + stack_start . | this | . . |_____________| stack_copy + | greenlet | + | | + | newer data | + | vvv | + + +Note that a greenlet's stack data is typically partly at its correct +place in the stack, and partly saved away in the heap, but always in +the above configuration: two blocks, the more recent one in the heap +and the older one still in the stack (either block may be empty). + +Greenlets are chained: each points to the previous greenlet, which is +the one that owns the data currently in the C stack above my +stack_stop. The currently running greenlet is the first element of +this chain. The main (initial) greenlet is the last one. Greenlets +whose stack is entirely in the heap can be skipped from the chain. + +The chain is not related to execution order, but only to the order +in which bits of C stack happen to belong to greenlets at a particular +point in time. + +The main greenlet doesn't have a stack_stop: it is responsible for the +complete rest of the C stack, and we don't know where it begins. We +use (char*) -1, the largest possible address. + +States: + stack_stop == NULL && stack_start == NULL: did not start yet + stack_stop != NULL && stack_start == NULL: already finished + stack_stop != NULL && stack_start != NULL: active + +The running greenlet's stack_start is undefined but not NULL. + + ***********************************************************/ + + + + +/***********************************************************/ + +/* Some functions must not be inlined: + * slp_restore_state, when inlined into slp_switch might cause + it to restore stack over its own local variables + * slp_save_state, when inlined would add its own local + variables to the saved stack, wasting space + * slp_switch, cannot be inlined for obvious reasons + * g_initialstub, when inlined would receive a pointer into its + own stack frame, leading to incomplete stack save/restore + +g_initialstub is a member function and declared virtual so that the +compiler always calls it through a vtable. + +slp_save_state and slp_restore_state are also member functions. They +are called from trampoline functions that themselves are declared as +not eligible for inlining. +*/ + +extern "C" { +static int GREENLET_NOINLINE(slp_save_state_trampoline)(char* stackref) +{ + return switching_thread_state->slp_save_state(stackref); +} +static void GREENLET_NOINLINE(slp_restore_state_trampoline)() +{ + switching_thread_state->slp_restore_state(); +} +} + + +/***********************************************************/ + + +#include "PyModule.cpp" + + + +static PyObject* +greenlet_internal_mod_init() noexcept +{ + static void* _PyGreenlet_API[PyGreenlet_API_pointers]; + + try { + CreatedModule m(greenlet_module_def); + + Require(PyType_Ready(&PyGreenlet_Type)); + Require(PyType_Ready(&PyGreenletUnswitchable_Type)); + + mod_globs = new greenlet::GreenletGlobals; + ThreadState::init(); + + m.PyAddObject("greenlet", PyGreenlet_Type); + m.PyAddObject("UnswitchableGreenlet", PyGreenletUnswitchable_Type); + m.PyAddObject("error", mod_globs->PyExc_GreenletError); + m.PyAddObject("GreenletExit", mod_globs->PyExc_GreenletExit); + + m.PyAddObject("GREENLET_USE_GC", 1); + m.PyAddObject("GREENLET_USE_TRACING", 1); + m.PyAddObject("GREENLET_USE_CONTEXT_VARS", 1L); + m.PyAddObject("GREENLET_USE_STANDARD_THREADING", 1L); + + OwnedObject clocks_per_sec = OwnedObject::consuming(PyLong_FromSsize_t(CLOCKS_PER_SEC)); + m.PyAddObject("CLOCKS_PER_SEC", clocks_per_sec); + + /* also publish module-level data as attributes of the greentype. */ + // XXX: This is weird, and enables a strange pattern of + // confusing the class greenlet with the module greenlet; with + // the exception of (possibly) ``getcurrent()``, this + // shouldn't be encouraged so don't add new items here. + for (const char* const* p = copy_on_greentype; *p; p++) { + OwnedObject o = m.PyRequireAttr(*p); + PyDict_SetItemString(PyGreenlet_Type.tp_dict, *p, o.borrow()); + } + + /* + * Expose C API + */ + + /* types */ + _PyGreenlet_API[PyGreenlet_Type_NUM] = (void*)&PyGreenlet_Type; + + /* exceptions */ + _PyGreenlet_API[PyExc_GreenletError_NUM] = (void*)mod_globs->PyExc_GreenletError; + _PyGreenlet_API[PyExc_GreenletExit_NUM] = (void*)mod_globs->PyExc_GreenletExit; + + /* methods */ + _PyGreenlet_API[PyGreenlet_New_NUM] = (void*)PyGreenlet_New; + _PyGreenlet_API[PyGreenlet_GetCurrent_NUM] = (void*)PyGreenlet_GetCurrent; + _PyGreenlet_API[PyGreenlet_Throw_NUM] = (void*)PyGreenlet_Throw; + _PyGreenlet_API[PyGreenlet_Switch_NUM] = (void*)PyGreenlet_Switch; + _PyGreenlet_API[PyGreenlet_SetParent_NUM] = (void*)PyGreenlet_SetParent; + + /* Previously macros, but now need to be functions externally. */ + _PyGreenlet_API[PyGreenlet_MAIN_NUM] = (void*)Extern_PyGreenlet_MAIN; + _PyGreenlet_API[PyGreenlet_STARTED_NUM] = (void*)Extern_PyGreenlet_STARTED; + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM] = (void*)Extern_PyGreenlet_ACTIVE; + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM] = (void*)Extern_PyGreenlet_GET_PARENT; + + /* XXX: Note that our module name is ``greenlet._greenlet``, but for + backwards compatibility with existing C code, we need the _C_API to + be directly in greenlet. + */ + const NewReference c_api_object(Require( + PyCapsule_New( + (void*)_PyGreenlet_API, + "greenlet._C_API", + NULL))); + m.PyAddObject("_C_API", c_api_object); + assert(c_api_object.REFCNT() == 2); + + // cerr << "Sizes:" + // << "\n\tGreenlet : " << sizeof(Greenlet) + // << "\n\tUserGreenlet : " << sizeof(UserGreenlet) + // << "\n\tMainGreenlet : " << sizeof(MainGreenlet) + // << "\n\tExceptionState : " << sizeof(greenlet::ExceptionState) + // << "\n\tPythonState : " << sizeof(greenlet::PythonState) + // << "\n\tStackState : " << sizeof(greenlet::StackState) + // << "\n\tSwitchingArgs : " << sizeof(greenlet::SwitchingArgs) + // << "\n\tOwnedObject : " << sizeof(greenlet::refs::OwnedObject) + // << "\n\tBorrowedObject : " << sizeof(greenlet::refs::BorrowedObject) + // << "\n\tPyGreenlet : " << sizeof(PyGreenlet) + // << endl; + + return m.borrow(); // But really it's the main reference. + } + catch (const LockInitError& e) { + PyErr_SetString(PyExc_MemoryError, e.what()); + return NULL; + } + catch (const PyErrOccurred&) { + return NULL; + } + +} + +extern "C" { + +PyMODINIT_FUNC +PyInit__greenlet(void) +{ + return greenlet_internal_mod_init(); +} + +}; // extern C + +#ifdef __clang__ +# pragma clang diagnostic pop +#elif defined(__GNUC__) +# pragma GCC diagnostic pop +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet.h b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet.h @@ -0,0 +1,164 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ + +/* Greenlet object interface */ + +#ifndef Py_GREENLETOBJECT_H +#define Py_GREENLETOBJECT_H + + +#include + +#ifdef __cplusplus +extern "C" { +#endif + +/* This is deprecated and undocumented. It does not change. */ +#define GREENLET_VERSION "1.0.0" + +#ifndef GREENLET_MODULE +#define implementation_ptr_t void* +#endif + +typedef struct _greenlet { + PyObject_HEAD + PyObject* weakreflist; + PyObject* dict; + implementation_ptr_t pimpl; +} PyGreenlet; + +#define PyGreenlet_Check(op) (op && PyObject_TypeCheck(op, &PyGreenlet_Type)) + + +/* C API functions */ + +/* Total number of symbols that are exported */ +#define PyGreenlet_API_pointers 12 + +#define PyGreenlet_Type_NUM 0 +#define PyExc_GreenletError_NUM 1 +#define PyExc_GreenletExit_NUM 2 + +#define PyGreenlet_New_NUM 3 +#define PyGreenlet_GetCurrent_NUM 4 +#define PyGreenlet_Throw_NUM 5 +#define PyGreenlet_Switch_NUM 6 +#define PyGreenlet_SetParent_NUM 7 + +#define PyGreenlet_MAIN_NUM 8 +#define PyGreenlet_STARTED_NUM 9 +#define PyGreenlet_ACTIVE_NUM 10 +#define PyGreenlet_GET_PARENT_NUM 11 + +#ifndef GREENLET_MODULE +/* This section is used by modules that uses the greenlet C API */ +static void** _PyGreenlet_API = NULL; + +# define PyGreenlet_Type \ + (*(PyTypeObject*)_PyGreenlet_API[PyGreenlet_Type_NUM]) + +# define PyExc_GreenletError \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletError_NUM]) + +# define PyExc_GreenletExit \ + ((PyObject*)_PyGreenlet_API[PyExc_GreenletExit_NUM]) + +/* + * PyGreenlet_New(PyObject *args) + * + * greenlet.greenlet(run, parent=None) + */ +# define PyGreenlet_New \ + (*(PyGreenlet * (*)(PyObject * run, PyGreenlet * parent)) \ + _PyGreenlet_API[PyGreenlet_New_NUM]) + +/* + * PyGreenlet_GetCurrent(void) + * + * greenlet.getcurrent() + */ +# define PyGreenlet_GetCurrent \ + (*(PyGreenlet * (*)(void)) _PyGreenlet_API[PyGreenlet_GetCurrent_NUM]) + +/* + * PyGreenlet_Throw( + * PyGreenlet *greenlet, + * PyObject *typ, + * PyObject *val, + * PyObject *tb) + * + * g.throw(...) + */ +# define PyGreenlet_Throw \ + (*(PyObject * (*)(PyGreenlet * self, \ + PyObject * typ, \ + PyObject * val, \ + PyObject * tb)) \ + _PyGreenlet_API[PyGreenlet_Throw_NUM]) + +/* + * PyGreenlet_Switch(PyGreenlet *greenlet, PyObject *args) + * + * g.switch(*args, **kwargs) + */ +# define PyGreenlet_Switch \ + (*(PyObject * \ + (*)(PyGreenlet * greenlet, PyObject * args, PyObject * kwargs)) \ + _PyGreenlet_API[PyGreenlet_Switch_NUM]) + +/* + * PyGreenlet_SetParent(PyObject *greenlet, PyObject *new_parent) + * + * g.parent = new_parent + */ +# define PyGreenlet_SetParent \ + (*(int (*)(PyGreenlet * greenlet, PyGreenlet * nparent)) \ + _PyGreenlet_API[PyGreenlet_SetParent_NUM]) + +/* + * PyGreenlet_GetParent(PyObject* greenlet) + * + * return greenlet.parent; + * + * This could return NULL even if there is no exception active. + * If it does not return NULL, you are responsible for decrementing the + * reference count. + */ +# define PyGreenlet_GetParent \ + (*(PyGreenlet* (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_GET_PARENT_NUM]) + +/* + * deprecated, undocumented alias. + */ +# define PyGreenlet_GET_PARENT PyGreenlet_GetParent + +# define PyGreenlet_MAIN \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_MAIN_NUM]) + +# define PyGreenlet_STARTED \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_STARTED_NUM]) + +# define PyGreenlet_ACTIVE \ + (*(int (*)(PyGreenlet*)) \ + _PyGreenlet_API[PyGreenlet_ACTIVE_NUM]) + + + + +/* Macro that imports greenlet and initializes C API */ +/* NOTE: This has actually moved to ``greenlet._greenlet._C_API``, but we + keep the older definition to be sure older code that might have a copy of + the header still works. */ +# define PyGreenlet_Import() \ + { \ + _PyGreenlet_API = (void**)PyCapsule_Import("greenlet._C_API", 0); \ + } + +#endif /* GREENLET_MODULE */ + +#ifdef __cplusplus +} +#endif +#endif /* !Py_GREENLETOBJECT_H */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_allocator.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_allocator.hpp new file mode 100644 index 0000000..dc2b969 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_allocator.hpp @@ -0,0 +1,89 @@ +#ifndef GREENLET_ALLOCATOR_HPP +#define GREENLET_ALLOCATOR_HPP + +#define PY_SSIZE_T_CLEAN +#include +#include +#include "greenlet_compiler_compat.hpp" +#include "greenlet_cpython_compat.hpp" + + +namespace greenlet +{ +#if defined(Py_GIL_DISABLED) +// Python on free threaded builds says this +// (https://docs.python.org/3/howto/free-threading-extensions.html#memory-allocation-apis): +// +// For thread-safety, the free-threaded build requires that only +// Python objects are allocated using the object domain, and that all +// Python object are allocated using that domain. +// +// This turns out to be important because the GC implementation on +// free threaded Python uses internal mimalloc APIs to find allocated +// objects. If we allocate non-PyObject objects using that API, then +// Bad Things could happen, including crashes and improper results. +// So in that case, we revert to standard C++ allocation. + + template + struct PythonAllocator : public std::allocator { + // This member is deprecated in C++17 and removed in C++20 + template< class U > + struct rebind { + typedef PythonAllocator other; + }; + }; + +#else + // This allocator is stateless; all instances are identical. + // It can *ONLY* be used when we're sure we're holding the GIL + // (Python's allocators require the GIL). + template + struct PythonAllocator : public std::allocator { + + PythonAllocator(const PythonAllocator& UNUSED(other)) + : std::allocator() + { + } + + PythonAllocator(const std::allocator other) + : std::allocator(other) + {} + + template + PythonAllocator(const std::allocator& other) + : std::allocator(other) + { + } + + PythonAllocator() : std::allocator() {} + + T* allocate(size_t number_objects, const void* UNUSED(hint)=0) + { + void* p; + if (number_objects == 1) + p = PyObject_Malloc(sizeof(T)); + else + p = PyMem_Malloc(sizeof(T) * number_objects); + return static_cast(p); + } + + void deallocate(T* t, size_t n) + { + void* p = t; + if (n == 1) { + PyObject_Free(p); + } + else + PyMem_Free(p); + } + // This member is deprecated in C++17 and removed in C++20 + template< class U > + struct rebind { + typedef PythonAllocator other; + }; + + }; +#endif // allocator type +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_compiler_compat.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_compiler_compat.hpp new file mode 100644 index 0000000..af24bd8 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_compiler_compat.hpp @@ -0,0 +1,98 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +#ifndef GREENLET_COMPILER_COMPAT_HPP +#define GREENLET_COMPILER_COMPAT_HPP + +/** + * Definitions to aid with compatibility with different compilers. + * + * .. caution:: Use extreme care with noexcept. + * Some compilers and runtimes, specifically gcc/libgcc/libstdc++ on + * Linux, implement stack unwinding by throwing an uncatchable + * exception, one that specifically does not appear to be an active + * exception to the rest of the runtime. If this happens while we're in a noexcept function, + * we have violated our dynamic exception contract, and so the runtime + * will call std::terminate(), which kills the process with the + * unhelpful message "terminate called without an active exception". + * + * This has happened in this scenario: A background thread is running + * a greenlet that has made a native call and released the GIL. + * Meanwhile, the main thread finishes and starts shutting down the + * interpreter. When the background thread is scheduled again and + * attempts to obtain the GIL, it notices that the interpreter is + * exiting and calls ``pthread_exit()``. This in turn starts to unwind + * the stack by throwing that exception. But we had the ``PyCall`` + * functions annotated as noexcept, so the runtime terminated us. + * + * #2 0x00007fab26fec2b7 in std::terminate() () from /lib/x86_64-linux-gnu/libstdc++.so.6 + * #3 0x00007fab26febb3c in __gxx_personality_v0 () from /lib/x86_64-linux-gnu/libstdc++.so.6 + * #4 0x00007fab26f34de6 in ?? () from /lib/x86_64-linux-gnu/libgcc_s.so.1 + * #6 0x00007fab276a34c6 in __GI___pthread_unwind at ./nptl/unwind.c:130 + * #7 0x00007fab2769bd3a in __do_cancel () at ../sysdeps/nptl/pthreadP.h:280 + * #8 __GI___pthread_exit (value=value@entry=0x0) at ./nptl/pthread_exit.c:36 + * #9 0x000000000052e567 in PyThread_exit_thread () at ../Python/thread_pthread.h:370 + * #10 0x00000000004d60b5 in take_gil at ../Python/ceval_gil.h:224 + * #11 0x00000000004d65f9 in PyEval_RestoreThread at ../Python/ceval.c:467 + * #12 0x000000000060cce3 in setipaddr at ../Modules/socketmodule.c:1203 + * #13 0x00000000006101cd in socket_gethostbyname + */ + +#include + +# define G_NO_COPIES_OF_CLS(Cls) private: \ + Cls(const Cls& other) = delete; \ + Cls& operator=(const Cls& other) = delete + +# define G_NO_ASSIGNMENT_OF_CLS(Cls) private: \ + Cls& operator=(const Cls& other) = delete + +# define G_NO_COPY_CONSTRUCTOR_OF_CLS(Cls) private: \ + Cls(const Cls& other) = delete; + + +// CAUTION: MSVC is stupidly picky: +// +// "The compiler ignores, without warning, any __declspec keywords +// placed after * or & and in front of the variable identifier in a +// declaration." +// (https://docs.microsoft.com/en-us/cpp/cpp/declspec?view=msvc-160) +// +// So pointer return types must be handled differently (because of the +// trailing *), or you get inscrutable compiler warnings like "error +// C2059: syntax error: ''" +// +// In C++ 11, there is a standard syntax for attributes, and +// GCC defines an attribute to use with this: [[gnu:noinline]]. +// In the future, this is expected to become standard. + +#if defined(__GNUC__) || defined(__clang__) +/* We used to check for GCC 4+ or 3.4+, but those compilers are + laughably out of date. Just assume they support it. */ +# define GREENLET_NOINLINE(name) __attribute__((noinline)) name +# define GREENLET_NOINLINE_P(rtype, name) rtype __attribute__((noinline)) name +# define UNUSED(x) UNUSED_ ## x __attribute__((__unused__)) +#elif defined(_MSC_VER) +/* We used to check for && (_MSC_VER >= 1300) but that's also out of date. */ +# define GREENLET_NOINLINE(name) __declspec(noinline) name +# define GREENLET_NOINLINE_P(rtype, name) __declspec(noinline) rtype name +# define UNUSED(x) UNUSED_ ## x +#endif + +#if defined(_MSC_VER) +# define G_NOEXCEPT_WIN32 noexcept +#else +# define G_NOEXCEPT_WIN32 +#endif + +#if defined(__GNUC__) && defined(__POWERPC__) && defined(__APPLE__) +// 32-bit PPC/MacOSX. Only known to be tested on unreleased versions +// of macOS 10.6 using a macports build gcc 14. It appears that +// running C++ destructors of thread-local variables is broken. + +// See https://github.com/python-greenlet/greenlet/pull/419 +# define GREENLET_BROKEN_THREAD_LOCAL_CLEANUP_JUST_LEAK 1 +#else +# define GREENLET_BROKEN_THREAD_LOCAL_CLEANUP_JUST_LEAK 0 +#endif + + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_cpython_compat.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_cpython_compat.hpp new file mode 100644 index 0000000..a3b3850 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_cpython_compat.hpp @@ -0,0 +1,150 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +#ifndef GREENLET_CPYTHON_COMPAT_H +#define GREENLET_CPYTHON_COMPAT_H + +/** + * Helpers for compatibility with multiple versions of CPython. + */ + +#define PY_SSIZE_T_CLEAN +#include "Python.h" + + +#if PY_VERSION_HEX >= 0x30A00B1 +# define GREENLET_PY310 1 +#else +# define GREENLET_PY310 0 +#endif + +/* +Python 3.10 beta 1 changed tstate->use_tracing to a nested cframe member. +See https://github.com/python/cpython/pull/25276 +We have to save and restore this as well. + +Python 3.13 removed PyThreadState.cframe (GH-108035). +*/ +#if GREENLET_PY310 && PY_VERSION_HEX < 0x30D0000 +# define GREENLET_USE_CFRAME 1 +#else +# define GREENLET_USE_CFRAME 0 +#endif + + +#if PY_VERSION_HEX >= 0x30B00A4 +/* +Greenlet won't compile on anything older than Python 3.11 alpha 4 (see +https://bugs.python.org/issue46090). Summary of breaking internal changes: +- Python 3.11 alpha 1 changed how frame objects are represented internally. + - https://github.com/python/cpython/pull/30122 +- Python 3.11 alpha 3 changed how recursion limits are stored. + - https://github.com/python/cpython/pull/29524 +- Python 3.11 alpha 4 changed how exception state is stored. It also includes a + change to help greenlet save and restore the interpreter frame "data stack". + - https://github.com/python/cpython/pull/30122 + - https://github.com/python/cpython/pull/30234 +*/ +# define GREENLET_PY311 1 +#else +# define GREENLET_PY311 0 +#endif + + +#if PY_VERSION_HEX >= 0x30C0000 +# define GREENLET_PY312 1 +#else +# define GREENLET_PY312 0 +#endif + +#if PY_VERSION_HEX >= 0x30D0000 +# define GREENLET_PY313 1 +#else +# define GREENLET_PY313 0 +#endif + +#if PY_VERSION_HEX >= 0x30E0000 +# define GREENLET_PY314 1 +#else +# define GREENLET_PY314 0 +#endif + +#ifndef Py_SET_REFCNT +/* Py_REFCNT and Py_SIZE macros are converted to functions +https://bugs.python.org/issue39573 */ +# define Py_SET_REFCNT(obj, refcnt) Py_REFCNT(obj) = (refcnt) +#endif + +#ifdef _Py_DEC_REFTOTAL +# define GREENLET_Py_DEC_REFTOTAL _Py_DEC_REFTOTAL +#else +/* _Py_DEC_REFTOTAL macro has been removed from Python 3.9 by: + https://github.com/python/cpython/commit/49932fec62c616ec88da52642339d83ae719e924 + + The symbol we use to replace it was removed by at least 3.12. +*/ +# ifdef Py_REF_DEBUG +# if GREENLET_PY312 +# define GREENLET_Py_DEC_REFTOTAL +# else +# define GREENLET_Py_DEC_REFTOTAL _Py_RefTotal-- +# endif +# else +# define GREENLET_Py_DEC_REFTOTAL +# endif +#endif +// Define these flags like Cython does if we're on an old version. +#ifndef Py_TPFLAGS_CHECKTYPES + #define Py_TPFLAGS_CHECKTYPES 0 +#endif +#ifndef Py_TPFLAGS_HAVE_INDEX + #define Py_TPFLAGS_HAVE_INDEX 0 +#endif +#ifndef Py_TPFLAGS_HAVE_NEWBUFFER + #define Py_TPFLAGS_HAVE_NEWBUFFER 0 +#endif + +#ifndef Py_TPFLAGS_HAVE_VERSION_TAG + #define Py_TPFLAGS_HAVE_VERSION_TAG 0 +#endif + +#define G_TPFLAGS_DEFAULT Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_VERSION_TAG | Py_TPFLAGS_CHECKTYPES | Py_TPFLAGS_HAVE_NEWBUFFER | Py_TPFLAGS_HAVE_GC + + +#if PY_VERSION_HEX < 0x03090000 +// The official version only became available in 3.9 +# define PyObject_GC_IsTracked(o) _PyObject_GC_IS_TRACKED(o) +#endif + + +// bpo-43760 added PyThreadState_EnterTracing() to Python 3.11.0a2 +#if PY_VERSION_HEX < 0x030B00A2 && !defined(PYPY_VERSION) +static inline void PyThreadState_EnterTracing(PyThreadState *tstate) +{ + tstate->tracing++; +#if PY_VERSION_HEX >= 0x030A00A1 + tstate->cframe->use_tracing = 0; +#else + tstate->use_tracing = 0; +#endif +} +#endif + +// bpo-43760 added PyThreadState_LeaveTracing() to Python 3.11.0a2 +#if PY_VERSION_HEX < 0x030B00A2 && !defined(PYPY_VERSION) +static inline void PyThreadState_LeaveTracing(PyThreadState *tstate) +{ + tstate->tracing--; + int use_tracing = (tstate->c_tracefunc != NULL + || tstate->c_profilefunc != NULL); +#if PY_VERSION_HEX >= 0x030A00A1 + tstate->cframe->use_tracing = use_tracing; +#else + tstate->use_tracing = use_tracing; +#endif +} +#endif + +#if !defined(Py_C_RECURSION_LIMIT) && defined(C_RECURSION_LIMIT) +# define Py_C_RECURSION_LIMIT C_RECURSION_LIMIT +#endif + +#endif /* GREENLET_CPYTHON_COMPAT_H */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_exceptions.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_exceptions.hpp new file mode 100644 index 0000000..617f07c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_exceptions.hpp @@ -0,0 +1,171 @@ +#ifndef GREENLET_EXCEPTIONS_HPP +#define GREENLET_EXCEPTIONS_HPP + +#define PY_SSIZE_T_CLEAN +#include +#include +#include + +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +#endif + +namespace greenlet { + + class PyErrOccurred : public std::runtime_error + { + public: + + // CAUTION: In debug builds, may run arbitrary Python code. + static const PyErrOccurred + from_current() + { + assert(PyErr_Occurred()); +#ifndef NDEBUG + // This is not exception safe, and + // not necessarily safe in general (what if it switches?) + // But we only do this in debug mode, where we are in + // tight control of what exceptions are getting raised and + // can prevent those issues. + + // You can't call PyObject_Str with a pending exception. + PyObject* typ; + PyObject* val; + PyObject* tb; + + PyErr_Fetch(&typ, &val, &tb); + PyObject* typs = PyObject_Str(typ); + PyObject* vals = PyObject_Str(val ? val : typ); + const char* typ_msg = PyUnicode_AsUTF8(typs); + const char* val_msg = PyUnicode_AsUTF8(vals); + PyErr_Restore(typ, val, tb); + + std::string msg(typ_msg); + msg += ": "; + msg += val_msg; + PyErrOccurred ex(msg); + Py_XDECREF(typs); + Py_XDECREF(vals); + + return ex; +#else + return PyErrOccurred(); +#endif + } + + PyErrOccurred() : std::runtime_error("") + { + assert(PyErr_Occurred()); + } + + PyErrOccurred(const std::string& msg) : std::runtime_error(msg) + { + assert(PyErr_Occurred()); + } + + PyErrOccurred(PyObject* exc_kind, const char* const msg) + : std::runtime_error(msg) + { + PyErr_SetString(exc_kind, msg); + } + + PyErrOccurred(PyObject* exc_kind, const std::string msg) + : std::runtime_error(msg) + { + // This copies the c_str, so we don't have any lifetime + // issues to worry about. + PyErr_SetString(exc_kind, msg.c_str()); + } + + PyErrOccurred(PyObject* exc_kind, + const std::string msg, //This is the format + //string; that's not + //usually safe! + + PyObject* borrowed_obj_one, PyObject* borrowed_obj_two) + : std::runtime_error(msg) + { + + //This is designed specifically for the + //``check_switch_allowed`` function. + + // PyObject_Str and PyObject_Repr are safe to call with + // NULL pointers; they return the string "" in that + // case. + // This function always returns null. + PyErr_Format(exc_kind, + msg.c_str(), + borrowed_obj_one, borrowed_obj_two); + } + }; + + class TypeError : public PyErrOccurred + { + public: + TypeError(const char* const what) + : PyErrOccurred(PyExc_TypeError, what) + { + } + TypeError(const std::string what) + : PyErrOccurred(PyExc_TypeError, what) + { + } + }; + + class ValueError : public PyErrOccurred + { + public: + ValueError(const char* const what) + : PyErrOccurred(PyExc_ValueError, what) + { + } + }; + + class AttributeError : public PyErrOccurred + { + public: + AttributeError(const char* const what) + : PyErrOccurred(PyExc_AttributeError, what) + { + } + }; + + /** + * Calls `Py_FatalError` when constructed, so you can't actually + * throw this. It just makes static analysis easier. + */ + class PyFatalError : public std::runtime_error + { + public: + PyFatalError(const char* const msg) + : std::runtime_error(msg) + { + Py_FatalError(msg); + } + }; + + static inline PyObject* + Require(PyObject* p, const std::string& msg="") + { + if (!p) { + throw PyErrOccurred(msg); + } + return p; + }; + + static inline void + Require(const int retval) + { + if (retval < 0) { + throw PyErrOccurred(); + } + }; + + +}; +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_internal.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_internal.hpp new file mode 100644 index 0000000..f2b15d5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_internal.hpp @@ -0,0 +1,107 @@ +/* -*- indent-tabs-mode: nil; tab-width: 4; -*- */ +#ifndef GREENLET_INTERNAL_H +#define GREENLET_INTERNAL_H +#ifdef __clang__ +# pragma clang diagnostic push +# pragma clang diagnostic ignored "-Wunused-function" +#endif + +/** + * Implementation helpers. + * + * C++ templates and inline functions should go here. + */ +#define PY_SSIZE_T_CLEAN +#include "greenlet_compiler_compat.hpp" +#include "greenlet_cpython_compat.hpp" +#include "greenlet_exceptions.hpp" +#include "TGreenlet.hpp" +#include "greenlet_allocator.hpp" + +#include +#include + +#define GREENLET_MODULE +struct _greenlet; +typedef struct _greenlet PyGreenlet; +namespace greenlet { + + class ThreadState; + // We can't use the PythonAllocator for this, because we push to it + // from the thread state destructor, which doesn't have the GIL, + // and Python's allocators can only be called with the GIL. + typedef std::vector cleanup_queue_t; + +}; + + +#define implementation_ptr_t greenlet::Greenlet* + + +#include "greenlet.h" + +void +greenlet::refs::MainGreenletExactChecker(void *p) +{ + if (!p) { + return; + } + // We control the class of the main greenlet exactly. + if (Py_TYPE(p) != &PyGreenlet_Type) { + std::string err("MainGreenlet: Expected exactly a greenlet, not a "); + err += Py_TYPE(p)->tp_name; + throw greenlet::TypeError(err); + } + + // Greenlets from dead threads no longer respond to main() with a + // true value; so in that case we need to perform an additional + // check. + Greenlet* g = static_cast(p)->pimpl; + if (g->main()) { + return; + } + if (!dynamic_cast(g)) { + std::string err("MainGreenlet: Expected exactly a main greenlet, not a "); + err += Py_TYPE(p)->tp_name; + throw greenlet::TypeError(err); + } +} + + + +template +inline greenlet::Greenlet* greenlet::refs::_OwnedGreenlet::operator->() const noexcept +{ + return reinterpret_cast(this->p)->pimpl; +} + +template +inline greenlet::Greenlet* greenlet::refs::_BorrowedGreenlet::operator->() const noexcept +{ + return reinterpret_cast(this->p)->pimpl; +} + +#include +#include + + +extern PyTypeObject PyGreenlet_Type; + + + +/** + * Forward declarations needed in multiple files. + */ +static PyObject* green_switch(PyGreenlet* self, PyObject* args, PyObject* kwargs); + + +#ifdef __clang__ +# pragma clang diagnostic pop +#endif + + +#endif + +// Local Variables: +// flycheck-clang-include-path: ("../../include" "/opt/local/Library/Frameworks/Python.framework/Versions/3.10/include/python3.10") +// End: diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_msvc_compat.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_msvc_compat.hpp new file mode 100644 index 0000000..c00245b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_msvc_compat.hpp @@ -0,0 +1,91 @@ +#ifndef GREENLET_MSVC_COMPAT_HPP +#define GREENLET_MSVC_COMPAT_HPP +/* + * Support for MSVC on Windows. + * + * Beginning with Python 3.14, some of the internal + * include files we need are not compatible with MSVC + * in C++ mode: + * + * internal\pycore_stackref.h(253): error C4576: a parenthesized type + * followed by an initializer list is a non-standard explicit type conversion syntax + * + * This file is included from ``internal/pycore_interpframe.h``, which + * we need for the ``_PyFrame_IsIncomplete`` API. + * + * Unfortunately, that API is a ``static inline`` function, as are a + * bunch of the functions it calls. The only solution seems to be to + * copy those definitions and the supporting inline functions here. + * + * Now, this makes us VERY fragile to changes in those functions. Because + * they're internal and static, the CPython devs might feel free to change + * them in even minor versions, meaning that we could runtime link and load, + * but still crash. We have that problem on all platforms though. It's just worse + * here because we have to keep copying the updated definitions. + */ +#include +#include "greenlet_cpython_compat.hpp" + +// This file is only included on 3.14+ + +extern "C" { + +// pycore_code.h ---------------- +#define _PyCode_CODE(CO) _Py_RVALUE((_Py_CODEUNIT *)(CO)->co_code_adaptive) +// End pycore_code.h ---------- + +// pycore_interpframe.h ---------- +#if !defined(Py_GIL_DISABLED) && defined(Py_STACKREF_DEBUG) + +#define Py_TAG_BITS 0 +#else +#define Py_TAG_BITS ((uintptr_t)1) +#define Py_TAG_DEFERRED (1) +#endif + + +static const _PyStackRef PyStackRef_NULL = { .bits = Py_TAG_DEFERRED}; +#define PyStackRef_IsNull(stackref) ((stackref).bits == PyStackRef_NULL.bits) + +static inline PyObject * +PyStackRef_AsPyObjectBorrow(_PyStackRef stackref) +{ + PyObject *cleared = ((PyObject *)((stackref).bits & (~Py_TAG_BITS))); + return cleared; +} + +static inline PyCodeObject *_PyFrame_GetCode(_PyInterpreterFrame *f) { + assert(!PyStackRef_IsNull(f->f_executable)); + PyObject *executable = PyStackRef_AsPyObjectBorrow(f->f_executable); + assert(PyCode_Check(executable)); + return (PyCodeObject *)executable; +} + + +static inline _Py_CODEUNIT * +_PyFrame_GetBytecode(_PyInterpreterFrame *f) +{ +#ifdef Py_GIL_DISABLED + PyCodeObject *co = _PyFrame_GetCode(f); + _PyCodeArray *tlbc = _PyCode_GetTLBCArray(co); + assert(f->tlbc_index >= 0 && f->tlbc_index < tlbc->size); + return (_Py_CODEUNIT *)tlbc->entries[f->tlbc_index]; +#else + return _PyCode_CODE(_PyFrame_GetCode(f)); +#endif +} + +static inline bool //_Py_NO_SANITIZE_THREAD +_PyFrame_IsIncomplete(_PyInterpreterFrame *frame) +{ + if (frame->owner >= FRAME_OWNED_BY_INTERPRETER) { + return true; + } + return frame->owner != FRAME_OWNED_BY_GENERATOR && + frame->instr_ptr < _PyFrame_GetBytecode(frame) + + _PyFrame_GetCode(frame)->_co_firsttraceable; +} +// pycore_interpframe.h ---------- + +} +#endif // GREENLET_MSVC_COMPAT_HPP diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_refs.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_refs.hpp new file mode 100644 index 0000000..b7e5e3f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_refs.hpp @@ -0,0 +1,1118 @@ +#ifndef GREENLET_REFS_HPP +#define GREENLET_REFS_HPP + +#define PY_SSIZE_T_CLEAN +#include + +#include + +//#include "greenlet_internal.hpp" +#include "greenlet_compiler_compat.hpp" +#include "greenlet_cpython_compat.hpp" +#include "greenlet_exceptions.hpp" + +struct _greenlet; +struct _PyMainGreenlet; + +typedef struct _greenlet PyGreenlet; +extern PyTypeObject PyGreenlet_Type; + + +#ifdef GREENLET_USE_STDIO +#include +using std::cerr; +using std::endl; +#endif + +namespace greenlet +{ + class Greenlet; + + namespace refs + { + // Type checkers throw a TypeError if the argument is not + // null, and isn't of the required Python type. + // (We can't use most of the defined type checkers + // like PyList_Check, etc, directly, because they are + // implemented as macros.) + typedef void (*TypeChecker)(void*); + + void + NoOpChecker(void*) + { + return; + } + + void + GreenletChecker(void *p) + { + if (!p) { + return; + } + + PyTypeObject* typ = Py_TYPE(p); + // fast, common path. (PyObject_TypeCheck is a macro or + // static inline function, and it also does a + // direct comparison of the type pointers, but its fast + // path only handles one type) + if (typ == &PyGreenlet_Type) { + return; + } + + if (!PyObject_TypeCheck(p, &PyGreenlet_Type)) { + std::string err("GreenletChecker: Expected any type of greenlet, not "); + err += Py_TYPE(p)->tp_name; + throw TypeError(err); + } + } + + void + MainGreenletExactChecker(void *p); + + template + class PyObjectPointer; + + template + class OwnedReference; + + + template + class BorrowedReference; + + typedef BorrowedReference BorrowedObject; + typedef OwnedReference OwnedObject; + + class ImmortalObject; + class ImmortalString; + + template + class _OwnedGreenlet; + + typedef _OwnedGreenlet OwnedGreenlet; + typedef _OwnedGreenlet OwnedMainGreenlet; + + template + class _BorrowedGreenlet; + + typedef _BorrowedGreenlet BorrowedGreenlet; + + void + ContextExactChecker(void *p) + { + if (!p) { + return; + } + if (!PyContext_CheckExact(p)) { + throw TypeError( + "greenlet context must be a contextvars.Context or None" + ); + } + } + + typedef OwnedReference OwnedContext; + } +} + +namespace greenlet { + + + namespace refs { + // A set of classes to make reference counting rules in python + // code explicit. + // + // Rules of use: + // (1) Functions returning a new reference that the caller of the + // function is expected to dispose of should return a + // ``OwnedObject`` object. This object automatically releases its + // reference when it goes out of scope. It works like a ``std::shared_ptr`` + // and can be copied or used as a function parameter (but don't do + // that). Note that constructing a ``OwnedObject`` from a + // PyObject* steals the reference. + // (2) Parameters to functions should be either a + // ``OwnedObject&``, or, more generally, a ``PyObjectPointer&``. + // If the function needs to create its own new reference, it can + // do so by copying to a local ``OwnedObject``. + // (3) Functions returning an existing pointer that is NOT + // incref'd, and which the caller MUST NOT decref, + // should return a ``BorrowedObject``. + + // XXX: The following two paragraphs do not hold for all platforms. + // Notably, 32-bit PPC Linux passes structs by reference, not by + // value, so this actually doesn't work. (Although that's the only + // platform that doesn't work on.) DO NOT ATTEMPT IT. The + // unfortunate consequence of that is that the slots which we + // *know* are already type safe will wind up calling the type + // checker function (when we had the slots accepting + // BorrowedGreenlet, this was bypassed), so this slows us down. + // TODO: Optimize this again. + + // For a class with a single pointer member, whose constructor + // does nothing but copy a pointer parameter into the member, and + // which can then be converted back to the pointer type, compilers + // generate code that's the same as just passing the pointer. + // That is, func(BorrowedObject x) called like ``PyObject* p = + // ...; f(p)`` has 0 overhead. Similarly, they "unpack" to the + // pointer type with 0 overhead. + // + // If there are no virtual functions, no complex inheritance (maybe?) and + // no destructor, these can be directly used as parameters in + // Python callbacks like tp_init: the layout is the same as a + // single pointer. Only subclasses with trivial constructors that + // do nothing but set the single pointer member are safe to use + // that way. + + + // This is the base class for things that can be done with a + // PyObject pointer. It assumes nothing about memory management. + // NOTE: Nothing is virtual, so subclasses shouldn't add new + // storage fields or try to override these methods. + template + class PyObjectPointer + { + public: + typedef T PyType; + protected: + T* p; + public: + PyObjectPointer(T* it=nullptr) : p(it) + { + TC(p); + } + + // We don't allow automatic casting to PyObject* at this + // level, because then we could be passed to Py_DECREF/INCREF, + // but we want nothing to do with memory management. If you + // know better, then you can use the get() method, like on a + // std::shared_ptr. Except we name it borrow() to clarify that + // if this is a reference-tracked object, the pointer you get + // back will go away when the object does. + // TODO: This should probably not exist here, but be moved + // down to relevant sub-types. + + T* borrow() const noexcept + { + return this->p; + } + + PyObject* borrow_o() const noexcept + { + return reinterpret_cast(this->p); + } + + T* operator->() const noexcept + { + return this->p; + } + + bool is_None() const noexcept + { + return this->p == Py_None; + } + + PyObject* acquire_or_None() const noexcept + { + PyObject* result = this->p ? reinterpret_cast(this->p) : Py_None; + Py_INCREF(result); + return result; + } + + explicit operator bool() const noexcept + { + return this->p != nullptr; + } + + bool operator!() const noexcept + { + return this->p == nullptr; + } + + Py_ssize_t REFCNT() const noexcept + { + return p ? Py_REFCNT(p) : -42; + } + + PyTypeObject* TYPE() const noexcept + { + return p ? Py_TYPE(p) : nullptr; + } + + inline OwnedObject PyStr() const noexcept; + inline const std::string as_str() const noexcept; + inline OwnedObject PyGetAttr(const ImmortalObject& name) const noexcept; + inline OwnedObject PyRequireAttr(const char* const name) const; + inline OwnedObject PyRequireAttr(const ImmortalString& name) const; + inline OwnedObject PyCall(const BorrowedObject& arg) const; + inline OwnedObject PyCall(PyGreenlet* arg) const ; + inline OwnedObject PyCall(PyObject* arg) const ; + // PyObject_Call(this, args, kwargs); + inline OwnedObject PyCall(const BorrowedObject args, + const BorrowedObject kwargs) const; + inline OwnedObject PyCall(const OwnedObject& args, + const OwnedObject& kwargs) const; + + protected: + void _set_raw_pointer(void* t) + { + TC(t); + p = reinterpret_cast(t); + } + void* _get_raw_pointer() const + { + return p; + } + }; + +#ifdef GREENLET_USE_STDIO + template + std::ostream& operator<<(std::ostream& os, const PyObjectPointer& s) + { + const std::type_info& t = typeid(s); + os << t.name() + << "(addr=" << s.borrow() + << ", refcnt=" << s.REFCNT() + << ", value=" << s.as_str() + << ")"; + + return os; + } +#endif + + template + inline bool operator==(const PyObjectPointer& lhs, const PyObject* const rhs) noexcept + { + return static_cast(lhs.borrow_o()) == static_cast(rhs); + } + + template + inline bool operator==(const PyObjectPointer& lhs, const PyObjectPointer& rhs) noexcept + { + return lhs.borrow_o() == rhs.borrow_o(); + } + + template + inline bool operator!=(const PyObjectPointer& lhs, + const PyObjectPointer& rhs) noexcept + { + return lhs.borrow_o() != rhs.borrow_o(); + } + + template + class OwnedReference : public PyObjectPointer + { + private: + friend class OwnedList; + + protected: + explicit OwnedReference(T* it) : PyObjectPointer(it) + { + } + + public: + + // Constructors + + static OwnedReference consuming(PyObject* p) + { + return OwnedReference(reinterpret_cast(p)); + } + + static OwnedReference owning(T* p) + { + OwnedReference result(p); + Py_XINCREF(result.p); + return result; + } + + OwnedReference() : PyObjectPointer(nullptr) + {} + + explicit OwnedReference(const PyObjectPointer<>& other) + : PyObjectPointer(nullptr) + { + T* op = other.borrow(); + TC(op); + this->p = other.borrow(); + Py_XINCREF(this->p); + } + + // It would be good to make use of the C++11 distinction + // between move and copy operations, e.g., constructing from a + // pointer should be a move operation. + // In the common case of ``OwnedObject x = Py_SomeFunction()``, + // the call to the copy constructor will be elided completely. + OwnedReference(const OwnedReference& other) + : PyObjectPointer(other.p) + { + Py_XINCREF(this->p); + } + + static OwnedReference None() + { + Py_INCREF(Py_None); + return OwnedReference(Py_None); + } + + // We can assign from exactly our type without any extra checking + OwnedReference& operator=(const OwnedReference& other) + { + Py_XINCREF(other.p); + const T* tmp = this->p; + this->p = other.p; + Py_XDECREF(tmp); + return *this; + } + + OwnedReference& operator=(const BorrowedReference other) + { + return this->operator=(other.borrow()); + } + + OwnedReference& operator=(T* const other) + { + TC(other); + Py_XINCREF(other); + T* tmp = this->p; + this->p = other; + Py_XDECREF(tmp); + return *this; + } + + // We can assign from an arbitrary reference type + // if it passes our check. + template + OwnedReference& operator=(const OwnedReference& other) + { + X* op = other.borrow(); + TC(op); + return this->operator=(reinterpret_cast(op)); + } + + inline void steal(T* other) + { + assert(this->p == nullptr); + TC(other); + this->p = other; + } + + T* relinquish_ownership() + { + T* result = this->p; + this->p = nullptr; + return result; + } + + T* acquire() const + { + // Return a new reference. + // TODO: This may go away when we have reference objects + // throughout the code. + Py_XINCREF(this->p); + return this->p; + } + + // Nothing else declares a destructor, we're the leaf, so we + // should be able to get away without virtual. + ~OwnedReference() + { + Py_CLEAR(this->p); + } + + void CLEAR() + { + Py_CLEAR(this->p); + assert(this->p == nullptr); + } + }; + + static inline + void operator<<=(PyObject*& target, OwnedObject& o) + { + target = o.relinquish_ownership(); + } + + + class NewReference : public OwnedObject + { + private: + G_NO_COPIES_OF_CLS(NewReference); + public: + // Consumes the reference. Only use this + // for API return values. + NewReference(PyObject* it) : OwnedObject(it) + { + } + }; + + class NewDictReference : public NewReference + { + private: + G_NO_COPIES_OF_CLS(NewDictReference); + public: + NewDictReference() : NewReference(PyDict_New()) + { + if (!this->p) { + throw PyErrOccurred(); + } + } + + void SetItem(const char* const key, PyObject* value) + { + Require(PyDict_SetItemString(this->p, key, value)); + } + + void SetItem(const PyObjectPointer<>& key, PyObject* value) + { + Require(PyDict_SetItem(this->p, key.borrow_o(), value)); + } + }; + + template + class _OwnedGreenlet: public OwnedReference + { + private: + protected: + _OwnedGreenlet(T* it) : OwnedReference(it) + {} + + public: + _OwnedGreenlet() : OwnedReference() + {} + + _OwnedGreenlet(const _OwnedGreenlet& other) : OwnedReference(other) + { + } + _OwnedGreenlet(OwnedMainGreenlet& other) : + OwnedReference(reinterpret_cast(other.acquire())) + { + } + _OwnedGreenlet(const BorrowedGreenlet& other); + // Steals a reference. + static _OwnedGreenlet consuming(PyGreenlet* it) + { + return _OwnedGreenlet(reinterpret_cast(it)); + } + + inline _OwnedGreenlet& operator=(const OwnedGreenlet& other) + { + return this->operator=(other.borrow()); + } + + inline _OwnedGreenlet& operator=(const BorrowedGreenlet& other); + + _OwnedGreenlet& operator=(const OwnedMainGreenlet& other) + { + PyGreenlet* owned = other.acquire(); + Py_XDECREF(this->p); + this->p = reinterpret_cast(owned); + return *this; + } + + _OwnedGreenlet& operator=(T* const other) + { + OwnedReference::operator=(other); + return *this; + } + + T* relinquish_ownership() + { + T* result = this->p; + this->p = nullptr; + return result; + } + + PyObject* relinquish_ownership_o() + { + return reinterpret_cast(relinquish_ownership()); + } + + inline Greenlet* operator->() const noexcept; + inline operator Greenlet*() const noexcept; + }; + + template + class BorrowedReference : public PyObjectPointer + { + public: + // Allow implicit creation from PyObject* pointers as we + // transition to using these classes. Also allow automatic + // conversion to PyObject* for passing to C API calls and even + // for Py_INCREF/DECREF, because we ourselves do no memory management. + BorrowedReference(T* it) : PyObjectPointer(it) + {} + + BorrowedReference(const PyObjectPointer& ref) : PyObjectPointer(ref.borrow()) + {} + + BorrowedReference() : PyObjectPointer(nullptr) + {} + + operator T*() const + { + return this->p; + } + }; + + typedef BorrowedReference BorrowedObject; + //typedef BorrowedReference BorrowedGreenlet; + + template + class _BorrowedGreenlet : public BorrowedReference + { + public: + _BorrowedGreenlet() : + BorrowedReference(nullptr) + {} + + _BorrowedGreenlet(T* it) : + BorrowedReference(it) + {} + + _BorrowedGreenlet(const BorrowedObject& it); + + _BorrowedGreenlet(const OwnedGreenlet& it) : + BorrowedReference(it.borrow()) + {} + + _BorrowedGreenlet& operator=(const BorrowedObject& other); + + // We get one of these for PyGreenlet, but one for PyObject + // is handy as well + operator PyObject*() const + { + return reinterpret_cast(this->p); + } + Greenlet* operator->() const noexcept; + operator Greenlet*() const noexcept; + }; + + typedef _BorrowedGreenlet BorrowedGreenlet; + + template + _OwnedGreenlet::_OwnedGreenlet(const BorrowedGreenlet& other) + : OwnedReference(reinterpret_cast(other.borrow())) + { + Py_XINCREF(this->p); + } + + + class BorrowedMainGreenlet + : public _BorrowedGreenlet + { + public: + BorrowedMainGreenlet(const OwnedMainGreenlet& it) : + _BorrowedGreenlet(it.borrow()) + {} + BorrowedMainGreenlet(PyGreenlet* it=nullptr) + : _BorrowedGreenlet(it) + {} + }; + + template + _OwnedGreenlet& _OwnedGreenlet::operator=(const BorrowedGreenlet& other) + { + return this->operator=(other.borrow()); + } + + + class ImmortalObject : public PyObjectPointer<> + { + private: + G_NO_ASSIGNMENT_OF_CLS(ImmortalObject); + public: + explicit ImmortalObject(PyObject* it) : PyObjectPointer<>(it) + { + } + + ImmortalObject(const ImmortalObject& other) + : PyObjectPointer<>(other.p) + { + + } + + /** + * Become the new owner of the object. Does not change the + * reference count. + */ + ImmortalObject& operator=(PyObject* it) + { + assert(this->p == nullptr); + this->p = it; + return *this; + } + + static ImmortalObject consuming(PyObject* it) + { + return ImmortalObject(it); + } + + inline operator PyObject*() const + { + return this->p; + } + }; + + class ImmortalString : public ImmortalObject + { + private: + G_NO_COPIES_OF_CLS(ImmortalString); + const char* str; + public: + ImmortalString(const char* const str) : + ImmortalObject(str ? Require(PyUnicode_InternFromString(str)) : nullptr) + { + this->str = str; + } + + inline ImmortalString& operator=(const char* const str) + { + if (!this->p) { + this->p = Require(PyUnicode_InternFromString(str)); + this->str = str; + } + else { + assert(this->str == str); + } + return *this; + } + + inline operator std::string() const + { + return this->str; + } + + }; + + class ImmortalEventName : public ImmortalString + { + private: + G_NO_COPIES_OF_CLS(ImmortalEventName); + public: + ImmortalEventName(const char* const str) : ImmortalString(str) + {} + }; + + class ImmortalException : public ImmortalObject + { + private: + G_NO_COPIES_OF_CLS(ImmortalException); + public: + ImmortalException(const char* const name, PyObject* base=nullptr) : + ImmortalObject(name + // Python 2.7 isn't const correct + ? Require(PyErr_NewException((char*)name, base, nullptr)) + : nullptr) + {} + + inline bool PyExceptionMatches() const + { + return PyErr_ExceptionMatches(this->p) > 0; + } + + }; + + template + inline OwnedObject PyObjectPointer::PyStr() const noexcept + { + if (!this->p) { + return OwnedObject(); + } + return OwnedObject::consuming(PyObject_Str(reinterpret_cast(this->p))); + } + + template + inline const std::string PyObjectPointer::as_str() const noexcept + { + // NOTE: This is not Python exception safe. + if (this->p) { + // The Python APIs return a cached char* value that's only valid + // as long as the original object stays around, and we're + // about to (probably) toss it. Hence the copy to std::string. + OwnedObject py_str = this->PyStr(); + if (!py_str) { + return "(nil)"; + } + return PyUnicode_AsUTF8(py_str.borrow()); + } + return "(nil)"; + } + + template + inline OwnedObject PyObjectPointer::PyGetAttr(const ImmortalObject& name) const noexcept + { + assert(this->p); + return OwnedObject::consuming(PyObject_GetAttr(reinterpret_cast(this->p), name)); + } + + template + inline OwnedObject PyObjectPointer::PyRequireAttr(const char* const name) const + { + assert(this->p); + return OwnedObject::consuming(Require(PyObject_GetAttrString(this->p, name), name)); + } + + template + inline OwnedObject PyObjectPointer::PyRequireAttr(const ImmortalString& name) const + { + assert(this->p); + return OwnedObject::consuming(Require( + PyObject_GetAttr( + reinterpret_cast(this->p), + name + ), + name + )); + } + + template + inline OwnedObject PyObjectPointer::PyCall(const BorrowedObject& arg) const + { + return this->PyCall(arg.borrow()); + } + + template + inline OwnedObject PyObjectPointer::PyCall(PyGreenlet* arg) const + { + return this->PyCall(reinterpret_cast(arg)); + } + + template + inline OwnedObject PyObjectPointer::PyCall(PyObject* arg) const + { + assert(this->p); + return OwnedObject::consuming(PyObject_CallFunctionObjArgs(this->p, arg, NULL)); + } + + template + inline OwnedObject PyObjectPointer::PyCall(const BorrowedObject args, + const BorrowedObject kwargs) const + { + assert(this->p); + return OwnedObject::consuming(PyObject_Call(this->p, args, kwargs)); + } + + template + inline OwnedObject PyObjectPointer::PyCall(const OwnedObject& args, + const OwnedObject& kwargs) const + { + assert(this->p); + return OwnedObject::consuming(PyObject_Call(this->p, args.borrow(), kwargs.borrow())); + } + + inline void + ListChecker(void * p) + { + if (!p) { + return; + } + if (!PyList_Check(p)) { + throw TypeError("Expected a list"); + } + } + + class OwnedList : public OwnedReference + { + private: + G_NO_ASSIGNMENT_OF_CLS(OwnedList); + public: + // TODO: Would like to use move. + explicit OwnedList(const OwnedObject& other) + : OwnedReference(other) + { + } + + OwnedList& operator=(const OwnedObject& other) + { + if (other && PyList_Check(other.p)) { + // Valid list. Own a new reference to it, discard the + // reference to what we did own. + PyObject* new_ptr = other.p; + Py_INCREF(new_ptr); + Py_XDECREF(this->p); + this->p = new_ptr; + } + else { + // Either the other object was NULL (an error) or it + // wasn't a list. Either way, we're now invalidated. + Py_XDECREF(this->p); + this->p = nullptr; + } + return *this; + } + + inline bool empty() const + { + return PyList_GET_SIZE(p) == 0; + } + + inline Py_ssize_t size() const + { + return PyList_GET_SIZE(p); + } + + inline BorrowedObject at(const Py_ssize_t index) const + { + return PyList_GET_ITEM(p, index); + } + + inline void clear() + { + PyList_SetSlice(p, 0, PyList_GET_SIZE(p), NULL); + } + }; + + // Use this to represent the module object used at module init + // time. + // This could either be a borrowed (Py2) or new (Py3) reference; + // either way, we don't want to do any memory management + // on it here, Python itself will handle that. + // XXX: Actually, that's not quite right. On Python 3, if an + // exception occurs before we return to the interpreter, this will + // leak; but all previous versions also had that problem. + class CreatedModule : public PyObjectPointer<> + { + private: + G_NO_COPIES_OF_CLS(CreatedModule); + public: + CreatedModule(PyModuleDef& mod_def) : PyObjectPointer<>( + Require(PyModule_Create(&mod_def))) + { + } + + // PyAddObject(): Add a reference to the object to the module. + // On return, the reference count of the object is unchanged. + // + // The docs warn that PyModule_AddObject only steals the + // reference on success, so if it fails after we've incref'd + // or allocated, we're responsible for the decref. + void PyAddObject(const char* name, const long new_bool) + { + OwnedObject p = OwnedObject::consuming(Require(PyBool_FromLong(new_bool))); + this->PyAddObject(name, p); + } + + void PyAddObject(const char* name, const OwnedObject& new_object) + { + // The caller already owns a reference they will decref + // when their variable goes out of scope, we still need to + // incref/decref. + this->PyAddObject(name, new_object.borrow()); + } + + void PyAddObject(const char* name, const ImmortalObject& new_object) + { + this->PyAddObject(name, new_object.borrow()); + } + + void PyAddObject(const char* name, PyTypeObject& type) + { + this->PyAddObject(name, reinterpret_cast(&type)); + } + + void PyAddObject(const char* name, PyObject* new_object) + { + Py_INCREF(new_object); + try { + Require(PyModule_AddObject(this->p, name, new_object)); + } + catch (const PyErrOccurred&) { + Py_DECREF(p); + throw; + } + } + }; + + class PyErrFetchParam : public PyObjectPointer<> + { + // Not an owned object, because we can't be initialized with + // one, and we only sometimes acquire ownership. + private: + G_NO_COPIES_OF_CLS(PyErrFetchParam); + public: + // To allow declaring these and passing them to + // PyErr_Fetch we implement the empty constructor, + // and the address operator. + PyErrFetchParam() : PyObjectPointer<>(nullptr) + { + } + + PyObject** operator&() + { + return &this->p; + } + + // This allows us to pass one directly without the &, + // BUT it has higher precedence than the bool operator + // if it's not explicit. + operator PyObject**() + { + return &this->p; + } + + // We don't want to be able to pass these to Py_DECREF and + // such so we don't have the implicit PyObject* conversion. + + inline PyObject* relinquish_ownership() + { + PyObject* result = this->p; + this->p = nullptr; + return result; + } + + ~PyErrFetchParam() + { + Py_XDECREF(p); + } + }; + + class OwnedErrPiece : public OwnedObject + { + private: + + public: + // Unlike OwnedObject, this increments the refcount. + OwnedErrPiece(PyObject* p=nullptr) : OwnedObject(p) + { + this->acquire(); + } + + PyObject** operator&() + { + return &this->p; + } + + inline operator PyObject*() const + { + return this->p; + } + + operator PyTypeObject*() const + { + return reinterpret_cast(this->p); + } + }; + + class PyErrPieces + { + private: + OwnedErrPiece type; + OwnedErrPiece instance; + OwnedErrPiece traceback; + bool restored; + public: + // Takes new references; if we're destroyed before + // restoring the error, we drop the references. + PyErrPieces(PyObject* t, PyObject* v, PyObject* tb) : + type(t), + instance(v), + traceback(tb), + restored(0) + { + this->normalize(); + } + + PyErrPieces() : + restored(0) + { + // PyErr_Fetch transfers ownership to us, so + // we don't actually need to INCREF; but we *do* + // need to DECREF if we're not restored. + PyErrFetchParam t, v, tb; + PyErr_Fetch(&t, &v, &tb); + type.steal(t.relinquish_ownership()); + instance.steal(v.relinquish_ownership()); + traceback.steal(tb.relinquish_ownership()); + } + + void PyErrRestore() + { + // can only do this once + assert(!this->restored); + this->restored = true; + PyErr_Restore( + this->type.relinquish_ownership(), + this->instance.relinquish_ownership(), + this->traceback.relinquish_ownership()); + assert(!this->type && !this->instance && !this->traceback); + } + + private: + void normalize() + { + // First, check the traceback argument, replacing None, + // with NULL + if (traceback.is_None()) { + traceback = nullptr; + } + + if (traceback && !PyTraceBack_Check(traceback.borrow())) { + throw PyErrOccurred(PyExc_TypeError, + "throw() third argument must be a traceback object"); + } + + if (PyExceptionClass_Check(type)) { + // If we just had a type, we'll now have a type and + // instance. + // The type's refcount will have gone up by one + // because of the instance and the instance will have + // a refcount of one. Either way, we owned, and still + // do own, exactly one reference. + PyErr_NormalizeException(&type, &instance, &traceback); + + } + else if (PyExceptionInstance_Check(type)) { + /* Raising an instance --- usually that means an + object that is a subclass of BaseException, but on + Python 2, that can also mean an arbitrary old-style + object. The value should be a dummy. */ + if (instance && !instance.is_None()) { + throw PyErrOccurred( + PyExc_TypeError, + "instance exception may not have a separate value"); + } + /* Normalize to raise , */ + this->instance = this->type; + this->type = PyExceptionInstance_Class(instance.borrow()); + + /* + It would be tempting to do this: + + Py_ssize_t type_count = Py_REFCNT(Py_TYPE(instance.borrow())); + this->type = PyExceptionInstance_Class(instance.borrow()); + assert(this->type.REFCNT() == type_count + 1); + + But that doesn't work on Python 2 in the case of + old-style instances: The result of Py_TYPE is going to + be the global shared that all + old-style classes have, while the return of Instance_Class() + will be the Python-level class object. The two are unrelated. + */ + } + else { + /* Not something you can raise. throw() fails. */ + PyErr_Format(PyExc_TypeError, + "exceptions must be classes, or instances, not %s", + Py_TYPE(type.borrow())->tp_name); + throw PyErrOccurred(); + } + } + }; + + // PyArg_Parse's O argument returns a borrowed reference. + class PyArgParseParam : public BorrowedObject + { + private: + G_NO_COPIES_OF_CLS(PyArgParseParam); + public: + explicit PyArgParseParam(PyObject* p=nullptr) : BorrowedObject(p) + { + } + + inline PyObject** operator&() + { + return &this->p; + } + }; + +};}; + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_slp_switch.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_slp_switch.hpp new file mode 100644 index 0000000..bd4b7ae --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_slp_switch.hpp @@ -0,0 +1,99 @@ +#ifndef GREENLET_SLP_SWITCH_HPP +#define GREENLET_SLP_SWITCH_HPP + +#include "greenlet_compiler_compat.hpp" +#include "greenlet_refs.hpp" + +/* + * the following macros are spliced into the OS/compiler + * specific code, in order to simplify maintenance. + */ +// We can save about 10% of the time it takes to switch greenlets if +// we thread the thread state through the slp_save_state() and the +// following slp_restore_state() calls from +// slp_switch()->g_switchstack() (which already needs to access it). +// +// However: +// +// that requires changing the prototypes and implementations of the +// switching functions. If we just change the prototype of +// slp_switch() to accept the argument and update the macros, without +// changing the implementation of slp_switch(), we get crashes on +// 64-bit Linux and 32-bit x86 (for reasons that aren't 100% clear); +// on the other hand, 64-bit macOS seems to be fine. Also, 64-bit +// windows is an issue because slp_switch is written fully in assembly +// and currently ignores its argument so some code would have to be +// adjusted there to pass the argument on to the +// ``slp_save_state_asm()`` function (but interestingly, because of +// the calling convention, the extra argument is just ignored and +// things function fine, albeit slower, if we just modify +// ``slp_save_state_asm`()` to fetch the pointer to pass to the +// macro.) +// +// Our compromise is to use a *glabal*, untracked, weak, pointer +// to the necessary thread state during the process of switching only. +// This is safe because we're protected by the GIL, and if we're +// running this code, the thread isn't exiting. This also nets us a +// 10-12% speed improvement. + +static greenlet::Greenlet* volatile switching_thread_state = nullptr; + + +extern "C" { +static int GREENLET_NOINLINE(slp_save_state_trampoline)(char* stackref); +static void GREENLET_NOINLINE(slp_restore_state_trampoline)(); +} + + +#define SLP_SAVE_STATE(stackref, stsizediff) \ +do { \ + assert(switching_thread_state); \ + stackref += STACK_MAGIC; \ + if (slp_save_state_trampoline((char*)stackref)) \ + return -1; \ + if (!switching_thread_state->active()) \ + return 1; \ + stsizediff = switching_thread_state->stack_start() - (char*)stackref; \ +} while (0) + +#define SLP_RESTORE_STATE() slp_restore_state_trampoline() + +#define SLP_EVAL +extern "C" { +#define slp_switch GREENLET_NOINLINE(slp_switch) +#include "slp_platformselect.h" +} +#undef slp_switch + +#ifndef STACK_MAGIC +# error \ + "greenlet needs to be ported to this platform, or taught how to detect your compiler properly." +#endif /* !STACK_MAGIC */ + + + +#ifdef EXTERNAL_ASM +/* CCP addition: Make these functions, to be called from assembler. + * The token include file for the given platform should enable the + * EXTERNAL_ASM define so that this is included. + */ +extern "C" { +intptr_t +slp_save_state_asm(intptr_t* ref) +{ + intptr_t diff; + SLP_SAVE_STATE(ref, diff); + return diff; +} + +void +slp_restore_state_asm(void) +{ + SLP_RESTORE_STATE(); +} + +extern int slp_switch(void); +}; +#endif + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_thread_support.hpp b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_thread_support.hpp new file mode 100644 index 0000000..3ded7d2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/greenlet_thread_support.hpp @@ -0,0 +1,31 @@ +#ifndef GREENLET_THREAD_SUPPORT_HPP +#define GREENLET_THREAD_SUPPORT_HPP + +/** + * Defines various utility functions to help greenlet integrate well + * with threads. This used to be needed when we supported Python + * 2.7 on Windows, which used a very old compiler. We wrote an + * alternative implementation using Python APIs and POSIX or Windows + * APIs, but that's no longer needed. So this file is a shadow of its + * former self --- but may be needed in the future. + */ + +#include +#include +#include + +#include "greenlet_compiler_compat.hpp" + +namespace greenlet { + typedef std::mutex Mutex; + typedef std::lock_guard LockGuard; + class LockInitError : public std::runtime_error + { + public: + LockInitError(const char* what) : std::runtime_error(what) + {}; + }; +}; + + +#endif /* GREENLET_THREAD_SUPPORT_HPP */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/__init__.py b/netdeploy/lib/python3.11/site-packages/greenlet/platform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/setup_switch_x64_masm.cmd b/netdeploy/lib/python3.11/site-packages/greenlet/platform/setup_switch_x64_masm.cmd new file mode 100644 index 0000000..0928595 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/setup_switch_x64_masm.cmd @@ -0,0 +1,2 @@ +call "C:\Program Files (x86)\Microsoft Visual Studio 9.0\VC\vcvarsall.bat" amd64 +ml64 /nologo /c /Fo switch_x64_masm.obj switch_x64_masm.asm diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_aarch64_gcc.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_aarch64_gcc.h new file mode 100644 index 0000000..058617c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_aarch64_gcc.h @@ -0,0 +1,124 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 07-Sep-16 Add clang support using x register naming. Fredrik Fornwall + * 13-Apr-13 Add support for strange GCC caller-save decisions + * 08-Apr-13 File creation. Michael Matz + * + * NOTES + * + * Simply save all callee saved registers + * + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 +#define REGS_TO_SAVE "x19", "x20", "x21", "x22", "x23", "x24", "x25", "x26", \ + "x27", "x28", "x30" /* aka lr */, \ + "v8", "v9", "v10", "v11", \ + "v12", "v13", "v14", "v15" + +/* + * Recall: + asm asm-qualifiers ( AssemblerTemplate + : OutputOperands + [ : InputOperands + [ : Clobbers ] ]) + + or (if asm-qualifiers contains 'goto') + + asm asm-qualifiers ( AssemblerTemplate + : OutputOperands + : InputOperands + : Clobbers + : GotoLabels) + + and OutputOperands are + + [ [asmSymbolicName] ] constraint (cvariablename) + + When a name is given, refer to it as ``%[the name]``. + When not given, ``%i`` where ``i`` is the zero-based index. + + constraints starting with ``=`` means only writing; ``+`` means + reading and writing. + + This is followed by ``r`` (must be register) or ``m`` (must be memory) + and these can be combined. + + The ``cvariablename`` is actually an lvalue expression. + + In AArch65, 31 general purpose registers. If named X0... they are + 64-bit. If named W0... they are the bottom 32 bits of the + corresponding 64 bit register. + + XZR and WZR are hardcoded to 0, and ignore writes. + + Arguments are in X0..X7. C++ uses X0 for ``this``. X0 holds simple return + values (?) + + Whenever a W register is written, the top half of the X register is zeroed. + */ + +static int +slp_switch(void) +{ + int err; + void *fp; + /* Windowz uses a 32-bit long on a 64-bit platform, unlike the rest of + the world, and in theory we can be compiled with GCC/llvm on 64-bit + windows. So we need a fixed-width type. + */ + int64_t *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("str x29, %0" : "=m"(fp) : : ); + __asm__ ("mov %0, sp" : "=r" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "add sp,sp,%0\n" + "add x29,x29,%0\n" + : + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + /* SLP_SAVE_STATE macro contains some return statements + (of -1 and 1). It falls through only when + the return value of slp_save_state() is zero, which + is placed in x0. + In that case we (slp_switch) also want to return zero + (also in x0 of course). + Now, some GCC versions (seen with 4.8) think it's a + good idea to save/restore x0 around the call to + slp_restore_state(), instead of simply zeroing it + at the return below. But slp_restore_state + writes random values to the stack slot used for this + save/restore (from when it once was saved above in + SLP_SAVE_STATE, when it was still uninitialized), so + "restoring" that precious zero actually makes us + return random values. There are some ways to make + GCC not use that zero value in the normal return path + (e.g. making err volatile, but that costs a little + stack space), and the simplest is to call a function + that returns an unknown value (which happens to be zero), + so the saved/restored value is unused. + + Thus, this line stores a 0 into the ``err`` variable + (which must be held in a register for this instruction, + of course). The ``w`` qualifier causes the instruction + to use W0 instead of X0, otherwise we get a warning + about a value size mismatch (because err is an int, + and aarch64 platforms are LP64: 32-bit int, 64 bit long + and pointer). + */ + __asm__ volatile ("mov %w0, #0" : "=r" (err)); + } + __asm__ volatile ("ldr x29, %0" : : "m" (fp) :); + __asm__ volatile ("" : : : REGS_TO_SAVE); + return err; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_alpha_unix.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_alpha_unix.h new file mode 100644 index 0000000..7e07abf --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_alpha_unix.h @@ -0,0 +1,30 @@ +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "$9", "$10", "$11", "$12", "$13", "$14", "$15", \ + "$f2", "$f3", "$f4", "$f5", "$f6", "$f7", "$f8", "$f9" + +static int +slp_switch(void) +{ + int ret; + long *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("mov $30, %0" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "addq $30, %0, $30\n\t" + : /* no outputs */ + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("mov $31, %0" : "=r" (ret) : ); + return ret; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_amd64_unix.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_amd64_unix.h new file mode 100644 index 0000000..d470110 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_amd64_unix.h @@ -0,0 +1,87 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 3-May-13 Ralf Schmitt + * Add support for strange GCC caller-save decisions + * (ported from switch_aarch64_gcc.h) + * 18-Aug-11 Alexey Borzenkov + * Correctly save rbp, csr and cw + * 01-Apr-04 Hye-Shik Chang + * Ported from i386 to amd64. + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for spark + * 31-Avr-02 Armin Rigo + * Added ebx, esi and edi register-saves. + * 01-Mar-02 Samual M. Rushing + * Ported from i386. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +/* #define STACK_MAGIC 3 */ +/* the above works fine with gcc 2.96, but 2.95.3 wants this */ +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "r12", "r13", "r14", "r15" + +static int +slp_switch(void) +{ + int err; + void* rbp; + void* rbx; + unsigned int csr; + unsigned short cw; + /* This used to be declared 'register', but that does nothing in + modern compilers and is explicitly forbidden in some new + standards. */ + long *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("fstcw %0" : "=m" (cw)); + __asm__ volatile ("stmxcsr %0" : "=m" (csr)); + __asm__ volatile ("movq %%rbp, %0" : "=m" (rbp)); + __asm__ volatile ("movq %%rbx, %0" : "=m" (rbx)); + __asm__ ("movq %%rsp, %0" : "=g" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "addq %0, %%rsp\n" + "addq %0, %%rbp\n" + : + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + __asm__ volatile ("xorq %%rax, %%rax" : "=a" (err)); + } + __asm__ volatile ("movq %0, %%rbx" : : "m" (rbx)); + __asm__ volatile ("movq %0, %%rbp" : : "m" (rbp)); + __asm__ volatile ("ldmxcsr %0" : : "m" (csr)); + __asm__ volatile ("fldcw %0" : : "m" (cw)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm32_gcc.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm32_gcc.h new file mode 100644 index 0000000..655003a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm32_gcc.h @@ -0,0 +1,79 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 14-Aug-06 File creation. Ported from Arm Thumb. Sylvain Baro + * 3-Sep-06 Commented out saving of r1-r3 (r4 already commented out) as I + * read that these do not need to be saved. Also added notes and + * errors related to the frame pointer. Richard Tew. + * + * NOTES + * + * It is not possible to detect if fp is used or not, so the supplied + * switch function needs to support it, so that you can remove it if + * it does not apply to you. + * + * POSSIBLE ERRORS + * + * "fp cannot be used in asm here" + * + * - Try commenting out "fp" in REGS_TO_SAVE. + * + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 +#define REG_SP "sp" +#define REG_SPSP "sp,sp" +#ifdef __thumb__ +#define REG_FP "r7" +#define REG_FPFP "r7,r7" +#define REGS_TO_SAVE_GENERAL "r4", "r5", "r6", "r8", "r9", "r10", "r11", "lr" +#else +#define REG_FP "fp" +#define REG_FPFP "fp,fp" +#define REGS_TO_SAVE_GENERAL "r4", "r5", "r6", "r7", "r8", "r9", "r10", "lr" +#endif +#if defined(__SOFTFP__) +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL +#elif defined(__VFP_FP__) +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL, "d8", "d9", "d10", "d11", \ + "d12", "d13", "d14", "d15" +#elif defined(__MAVERICK__) +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL, "mvf4", "mvf5", "mvf6", "mvf7", \ + "mvf8", "mvf9", "mvf10", "mvf11", \ + "mvf12", "mvf13", "mvf14", "mvf15" +#else +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL, "f4", "f5", "f6", "f7" +#endif + +static int +#ifdef __GNUC__ +__attribute__((optimize("no-omit-frame-pointer"))) +#endif +slp_switch(void) +{ + void *fp; + int *stackref, stsizediff; + int result; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("mov r0," REG_FP "\n\tstr r0,%0" : "=m" (fp) : : "r0"); + __asm__ ("mov %0," REG_SP : "=r" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "add " REG_SPSP ",%0\n" + "add " REG_FPFP ",%0\n" + : + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("ldr r0,%1\n\tmov " REG_FP ",r0\n\tmov %0, #0" : "=r" (result) : "m" (fp) : "r0"); + __asm__ volatile ("" : : : REGS_TO_SAVE); + return result; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm32_ios.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm32_ios.h new file mode 100644 index 0000000..9e640e1 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm32_ios.h @@ -0,0 +1,67 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 31-May-15 iOS support. Ported from arm32. Proton + * + * NOTES + * + * It is not possible to detect if fp is used or not, so the supplied + * switch function needs to support it, so that you can remove it if + * it does not apply to you. + * + * POSSIBLE ERRORS + * + * "fp cannot be used in asm here" + * + * - Try commenting out "fp" in REGS_TO_SAVE. + * + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 0 +#define REG_SP "sp" +#define REG_SPSP "sp,sp" +#define REG_FP "r7" +#define REG_FPFP "r7,r7" +#define REGS_TO_SAVE_GENERAL "r4", "r5", "r6", "r8", "r10", "r11", "lr" +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL, "d8", "d9", "d10", "d11", \ + "d12", "d13", "d14", "d15" + +static int +#ifdef __GNUC__ +__attribute__((optimize("no-omit-frame-pointer"))) +#endif +slp_switch(void) +{ + void *fp; + int *stackref, stsizediff, result; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("str " REG_FP ",%0" : "=m" (fp)); + __asm__ ("mov %0," REG_SP : "=r" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "add " REG_SPSP ",%0\n" + "add " REG_FPFP ",%0\n" + : + : "r" (stsizediff) + : REGS_TO_SAVE /* Clobber registers, force compiler to + * recalculate address of void *fp from REG_SP or REG_FP */ + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ( + "ldr " REG_FP ", %1\n\t" + "mov %0, #0" + : "=r" (result) + : "m" (fp) + : REGS_TO_SAVE /* Force compiler to restore saved registers after this */ + ); + return result; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.asm b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.asm new file mode 100644 index 0000000..29f9c22 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.asm @@ -0,0 +1,53 @@ + AREA switch_arm64_masm, CODE, READONLY; + GLOBAL slp_switch [FUNC] + EXTERN slp_save_state_asm + EXTERN slp_restore_state_asm + +slp_switch + ; push callee saved registers to stack + stp x19, x20, [sp, #-16]! + stp x21, x22, [sp, #-16]! + stp x23, x24, [sp, #-16]! + stp x25, x26, [sp, #-16]! + stp x27, x28, [sp, #-16]! + stp x29, x30, [sp, #-16]! + stp d8, d9, [sp, #-16]! + stp d10, d11, [sp, #-16]! + stp d12, d13, [sp, #-16]! + stp d14, d15, [sp, #-16]! + + ; call slp_save_state_asm with stack pointer + mov x0, sp + bl slp_save_state_asm + + ; early return for return value of 1 and -1 + cmp x0, #-1 + b.eq RETURN + cmp x0, #1 + b.eq RETURN + + ; increment stack and frame pointer + add sp, sp, x0 + add x29, x29, x0 + + bl slp_restore_state_asm + + ; store return value for successful completion of routine + mov x0, #0 + +RETURN + ; pop registers from stack + ldp d14, d15, [sp], #16 + ldp d12, d13, [sp], #16 + ldp d10, d11, [sp], #16 + ldp d8, d9, [sp], #16 + ldp x29, x30, [sp], #16 + ldp x27, x28, [sp], #16 + ldp x25, x26, [sp], #16 + ldp x23, x24, [sp], #16 + ldp x21, x22, [sp], #16 + ldp x19, x20, [sp], #16 + + ret + + END diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.obj b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.obj new file mode 100644 index 0000000000000000000000000000000000000000..f6f220e4310baaa9756110685ce7d6a2bdf90c37 GIT binary patch literal 746 zcma)4PiqrF6n~qoo~*PNZ{i+=wji4b#Xu2~wiJpGk)*AMF07NyB(9n1#+i+!)I;v| zBKQG3?t1eB$T(lYgGb4+lu{_QmQrebldQB_4?cMF-uu0IZ{DA2e8@rZ+e^~30488W z`B{KPh%yV{HEIpyeum^wI#7P*HfX)ux?9U&c!SFK-$o|OFtKn{Q|a-#N>2inp0-tb zCRKXAtg2SolaoLv$Ll&ds_Epj?SH+8K{t?XSjKaFsEy%yh}=V70BaHj zEY5kWk_zcJo{$SSUL~=K(zW|tnhm$rA z<%dZ$q?>RX*18r{!azhaYR1lVb;g;mR-6h!#F>|p@;aje%0a|CZrE7s+SXuT>MS=Y ziQPiMY-5DD&5+S7^H03fy7qt7ir}L34kK|h68s-MU>{lXOqlr?!Y=`~WwviNenFS_ zZalVSHh-0FU4lj#X8u5`ODn6@#{f?dHE&%XdP~_I3;$RS9-(z*>>ydkm*f@oWlUn~ Qn+^;lsEi}=H#!Q3U&UU-WdHyG literal 0 HcmV?d00001 diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_msvc.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_msvc.h new file mode 100644 index 0000000..7ab7f45 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_arm64_msvc.h @@ -0,0 +1,17 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 21-Oct-21 Niyas Sait + * First version to enable win/arm64 support. + */ + +#define STACK_REFPLUS 1 +#define STACK_MAGIC 0 + +/* Use the generic support for an external assembly language slp_switch function. */ +#define EXTERNAL_ASM + +#ifdef SLP_EVAL +/* This always uses the external masm assembly file. */ +#endif \ No newline at end of file diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_csky_gcc.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_csky_gcc.h new file mode 100644 index 0000000..ac469d3 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_csky_gcc.h @@ -0,0 +1,48 @@ +#ifdef SLP_EVAL +#define STACK_MAGIC 0 +#define REG_FP "r8" +#ifdef __CSKYABIV2__ +#define REGS_TO_SAVE_GENERAL "r4", "r5", "r6", "r7", "r9", "r10", "r11", "r15",\ + "r16", "r17", "r18", "r19", "r20", "r21", "r22",\ + "r23", "r24", "r25" + +#if defined (__CSKY_HARD_FLOAT__) || (__CSKY_VDSP__) +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL, "vr8", "vr9", "vr10", "vr11", "vr12",\ + "vr13", "vr14", "vr15" +#else +#define REGS_TO_SAVE REGS_TO_SAVE_GENERAL +#endif +#else +#define REGS_TO_SAVE "r9", "r10", "r11", "r12", "r13", "r15" +#endif + + +static int +#ifdef __GNUC__ +__attribute__((optimize("no-omit-frame-pointer"))) +#endif +slp_switch(void) +{ + int *stackref, stsizediff; + int result; + + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ ("mov %0, sp" : "=r" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "addu sp,%0\n" + "addu "REG_FP",%0\n" + : + : "r" (stsizediff) + ); + + SLP_RESTORE_STATE(); + } + __asm__ volatile ("movi %0, 0" : "=r" (result)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + + return result; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_loongarch64_linux.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_loongarch64_linux.h new file mode 100644 index 0000000..9eaf34e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_loongarch64_linux.h @@ -0,0 +1,31 @@ +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "s0", "s1", "s2", "s3", "s4", "s5", \ + "s6", "s7", "s8", "fp", \ + "f24", "f25", "f26", "f27", "f28", "f29", "f30", "f31" + +static int +slp_switch(void) +{ + int ret; + long *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("move %0, $sp" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "add.d $sp, $sp, %0\n\t" + : /* no outputs */ + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("move %0, $zero" : "=r" (ret) : ); + return ret; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_m68k_gcc.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_m68k_gcc.h new file mode 100644 index 0000000..da761c2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_m68k_gcc.h @@ -0,0 +1,38 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 2014-01-06 Andreas Schwab + * File created. + */ + +#ifdef SLP_EVAL + +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "%d2", "%d3", "%d4", "%d5", "%d6", "%d7", \ + "%a2", "%a3", "%a4" + +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + void *fp, *a5; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("move.l %%fp, %0" : "=m"(fp)); + __asm__ volatile ("move.l %%a5, %0" : "=m"(a5)); + __asm__ ("move.l %%sp, %0" : "=r"(stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ("add.l %0, %%sp; add.l %0, %%fp" : : "r"(stsizediff)); + SLP_RESTORE_STATE(); + __asm__ volatile ("clr.l %0" : "=g" (err)); + } + __asm__ volatile ("move.l %0, %%a5" : : "m"(a5)); + __asm__ volatile ("move.l %0, %%fp" : : "m"(fp)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + return err; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_mips_unix.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_mips_unix.h new file mode 100644 index 0000000..b9003e9 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_mips_unix.h @@ -0,0 +1,64 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 20-Sep-14 Matt Madison + * Re-code the saving of the gp register for MIPS64. + * 05-Jan-08 Thiemo Seufer + * Ported from ppc. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "$16", "$17", "$18", "$19", "$20", "$21", "$22", \ + "$23", "$30" +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; +#ifdef __mips64 + uint64_t gpsave; +#endif + __asm__ __volatile__ ("" : : : REGS_TO_SAVE); +#ifdef __mips64 + __asm__ __volatile__ ("sd $28,%0" : "=m" (gpsave) : : ); +#endif + __asm__ ("move %0, $29" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ __volatile__ ( +#ifdef __mips64 + "daddu $29, %0\n" +#else + "addu $29, %0\n" +#endif + : /* no outputs */ + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } +#ifdef __mips64 + __asm__ __volatile__ ("ld $28,%0" : : "m" (gpsave) : ); +#endif + __asm__ __volatile__ ("" : : : REGS_TO_SAVE); + __asm__ __volatile__ ("move %0, $0" : "=r" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_aix.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_aix.h new file mode 100644 index 0000000..e7e0b87 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_aix.h @@ -0,0 +1,103 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 16-Oct-20 Jesse Gorzinski + * Copied from Linux PPC64 implementation + * 04-Sep-18 Alexey Borzenkov + * Workaround a gcc bug using manual save/restore of r30 + * 21-Mar-18 Tulio Magno Quites Machado Filho + * Added r30 to the list of saved registers in order to fully comply with + * both ppc64 ELFv1 ABI and the ppc64le ELFv2 ABI, that classify this + * register as a nonvolatile register used for local variables. + * 21-Mar-18 Laszlo Boszormenyi + * Save r2 (TOC pointer) manually. + * 10-Dec-13 Ulrich Weigand + * Support ELFv2 ABI. Save float/vector registers. + * 09-Mar-12 Michael Ellerman + * 64-bit implementation, copied from 32-bit. + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 04-Oct-02 Gustavo Niemeyer + * Ported from MacOS version. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + * 31-Jul-12 Trevor Bowen + * Changed memory constraints to register only. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 6 + +#if defined(__ALTIVEC__) +#define ALTIVEC_REGS \ + "v20", "v21", "v22", "v23", "v24", "v25", "v26", "v27", \ + "v28", "v29", "v30", "v31", +#else +#define ALTIVEC_REGS +#endif + +#define REGS_TO_SAVE "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "r31", \ + "fr14", "fr15", "fr16", "fr17", "fr18", "fr19", "fr20", "fr21", \ + "fr22", "fr23", "fr24", "fr25", "fr26", "fr27", "fr28", "fr29", \ + "fr30", "fr31", \ + ALTIVEC_REGS \ + "cr2", "cr3", "cr4" + +static int +slp_switch(void) +{ + int err; + long *stackref, stsizediff; + void * toc; + void * r30; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("std 2, %0" : "=m" (toc)); + __asm__ volatile ("std 30, %0" : "=m" (r30)); + __asm__ ("mr %0, 1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "mr 11, %0\n" + "add 1, 1, 11\n" + : /* no outputs */ + : "r" (stsizediff) + : "11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("ld 30, %0" : : "m" (r30)); + __asm__ volatile ("ld 2, %0" : : "m" (toc)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_linux.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_linux.h new file mode 100644 index 0000000..3c324d0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_linux.h @@ -0,0 +1,105 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 04-Sep-18 Alexey Borzenkov + * Workaround a gcc bug using manual save/restore of r30 + * 21-Mar-18 Tulio Magno Quites Machado Filho + * Added r30 to the list of saved registers in order to fully comply with + * both ppc64 ELFv1 ABI and the ppc64le ELFv2 ABI, that classify this + * register as a nonvolatile register used for local variables. + * 21-Mar-18 Laszlo Boszormenyi + * Save r2 (TOC pointer) manually. + * 10-Dec-13 Ulrich Weigand + * Support ELFv2 ABI. Save float/vector registers. + * 09-Mar-12 Michael Ellerman + * 64-bit implementation, copied from 32-bit. + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 04-Oct-02 Gustavo Niemeyer + * Ported from MacOS version. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + * 31-Jul-12 Trevor Bowen + * Changed memory constraints to register only. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#if _CALL_ELF == 2 +#define STACK_MAGIC 4 +#else +#define STACK_MAGIC 6 +#endif + +#if defined(__ALTIVEC__) +#define ALTIVEC_REGS \ + "v20", "v21", "v22", "v23", "v24", "v25", "v26", "v27", \ + "v28", "v29", "v30", "v31", +#else +#define ALTIVEC_REGS +#endif + +#define REGS_TO_SAVE "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "r31", \ + "fr14", "fr15", "fr16", "fr17", "fr18", "fr19", "fr20", "fr21", \ + "fr22", "fr23", "fr24", "fr25", "fr26", "fr27", "fr28", "fr29", \ + "fr30", "fr31", \ + ALTIVEC_REGS \ + "cr2", "cr3", "cr4" + +static int +slp_switch(void) +{ + int err; + long *stackref, stsizediff; + void * toc; + void * r30; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("std 2, %0" : "=m" (toc)); + __asm__ volatile ("std 30, %0" : "=m" (r30)); + __asm__ ("mr %0, 1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "mr 11, %0\n" + "add 1, 1, 11\n" + : /* no outputs */ + : "r" (stsizediff) + : "11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("ld 30, %0" : : "m" (r30)); + __asm__ volatile ("ld 2, %0" : : "m" (toc)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_aix.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_aix.h new file mode 100644 index 0000000..6d93c13 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_aix.h @@ -0,0 +1,87 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 07-Mar-11 Floris Bruynooghe + * Do not add stsizediff to general purpose + * register (GPR) 30 as this is a non-volatile and + * unused by the PowerOpen Environment, therefore + * this was modifying a user register instead of the + * frame pointer (which does not seem to exist). + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 04-Oct-02 Gustavo Niemeyer + * Ported from MacOS version. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 3 + +/* !!!!WARNING!!!! need to add "r31" in the next line if this header file + * is meant to be compiled non-dynamically! + */ +#define REGS_TO_SAVE "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "cr2", "cr3", "cr4" +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ ("mr %0, 1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "mr 11, %0\n" + "add 1, 1, 11\n" + : /* no outputs */ + : "r" (stsizediff) + : "11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_linux.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_linux.h new file mode 100644 index 0000000..e83ad70 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_linux.h @@ -0,0 +1,84 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 04-Oct-02 Gustavo Niemeyer + * Ported from MacOS version. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + * 31-Jul-12 Trevor Bowen + * Changed memory constraints to register only. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 3 + +/* !!!!WARNING!!!! need to add "r31" in the next line if this header file + * is meant to be compiled non-dynamically! + */ +#define REGS_TO_SAVE "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "cr2", "cr3", "cr4" +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ ("mr %0, 1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "mr 11, %0\n" + "add 1, 1, 11\n" + "add 30, 30, 11\n" + : /* no outputs */ + : "r" (stsizediff) + : "11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_macosx.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_macosx.h new file mode 100644 index 0000000..bd414c6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_macosx.h @@ -0,0 +1,82 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 3 + +/* !!!!WARNING!!!! need to add "r31" in the next line if this header file + * is meant to be compiled non-dynamically! + */ +#define REGS_TO_SAVE "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "cr2", "cr3", "cr4" + +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ ("; asm block 2\n\tmr %0, r1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "; asm block 3\n" + "\tmr r11, %0\n" + "\tadd r1, r1, r11\n" + "\tadd r30, r30, r11\n" + : /* no outputs */ + : "r" (stsizediff) + : "r11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_unix.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_unix.h new file mode 100644 index 0000000..bb18808 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_ppc_unix.h @@ -0,0 +1,82 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'r31' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 14-Jan-04 Bob Ippolito + * added cr2-cr4 to the registers to be saved. + * Open questions: Should we save FP registers? + * What about vector registers? + * Differences between darwin and unix? + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 04-Oct-02 Gustavo Niemeyer + * Ported from MacOS version. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 29-Jun-02 Christian Tismer + * Added register 13-29, 31 saves. The same way as + * Armin Rigo did for the x86_unix version. + * This seems to be now fully functional! + * 04-Mar-02 Hye-Shik Chang + * Ported from i386. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 3 + +/* !!!!WARNING!!!! need to add "r31" in the next line if this header file + * is meant to be compiled non-dynamically! + */ +#define REGS_TO_SAVE "r13", "r14", "r15", "r16", "r17", "r18", "r19", "r20", \ + "r21", "r22", "r23", "r24", "r25", "r26", "r27", "r28", "r29", \ + "cr2", "cr3", "cr4" +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ ("mr %0, 1" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "mr 11, %0\n" + "add 1, 1, 11\n" + "add 30, 30, 11\n" + : /* no outputs */ + : "r" (stsizediff) + : "11" + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("li %0, 0" : "=r" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_riscv_unix.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_riscv_unix.h new file mode 100644 index 0000000..8761122 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_riscv_unix.h @@ -0,0 +1,41 @@ +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "s1", "s2", "s3", "s4", "s5", \ + "s6", "s7", "s8", "s9", "s10", "s11", "fs0", "fs1", \ + "fs2", "fs3", "fs4", "fs5", "fs6", "fs7", "fs8", "fs9", \ + "fs10", "fs11" + +static int +slp_switch(void) +{ + int ret; + long fp; + long *stackref, stsizediff; + + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("mv %0, fp" : "=r" (fp) : ); + __asm__ volatile ("mv %0, sp" : "=r" (stackref) : ); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "add sp, sp, %0\n\t" + "add fp, fp, %0\n\t" + : /* no outputs */ + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); +#if __riscv_xlen == 32 + __asm__ volatile ("lw fp, %0" : : "m" (fp)); +#else + __asm__ volatile ("ld fp, %0" : : "m" (fp)); +#endif + __asm__ volatile ("mv %0, zero" : "=r" (ret) : ); + return ret; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_s390_unix.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_s390_unix.h new file mode 100644 index 0000000..9199367 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_s390_unix.h @@ -0,0 +1,87 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 25-Jan-12 Alexey Borzenkov + * Fixed Linux/S390 port to work correctly with + * different optimization options both on 31-bit + * and 64-bit. Thanks to Stefan Raabe for lots + * of testing. + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 06-Oct-02 Gustavo Niemeyer + * Ported to Linux/S390. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#ifdef __s390x__ +#define STACK_MAGIC 20 /* 20 * 8 = 160 bytes of function call area */ +#else +#define STACK_MAGIC 24 /* 24 * 4 = 96 bytes of function call area */ +#endif + +/* Technically, r11-r13 also need saving, but function prolog starts + with stm(g) and since there are so many saved registers already + it won't be optimized, resulting in all r6-r15 being saved */ +#define REGS_TO_SAVE "r6", "r7", "r8", "r9", "r10", "r14", \ + "f0", "f1", "f2", "f3", "f4", "f5", "f6", "f7", \ + "f8", "f9", "f10", "f11", "f12", "f13", "f14", "f15" + +static int +slp_switch(void) +{ + int ret; + long *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); +#ifdef __s390x__ + __asm__ volatile ("lgr %0, 15" : "=r" (stackref) : ); +#else + __asm__ volatile ("lr %0, 15" : "=r" (stackref) : ); +#endif + { + SLP_SAVE_STATE(stackref, stsizediff); +/* N.B. + r11 may be used as the frame pointer, and in that case it cannot be + clobbered and needs offsetting just like the stack pointer (but in cases + where frame pointer isn't used we might clobber it accidentally). What's + scary is that r11 is 2nd (and even 1st when GOT is used) callee saved + register that gcc would chose for surviving function calls. However, + since r6-r10 are clobbered above, their cost for reuse is reduced, so + gcc IRA will chose them over r11 (not seeing r11 is implicitly saved), + making it relatively safe to offset in all cases. :) */ + __asm__ volatile ( +#ifdef __s390x__ + "agr 15, %0\n\t" + "agr 11, %0" +#else + "ar 15, %0\n\t" + "ar 11, %0" +#endif + : /* no outputs */ + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("lhi %0, 0" : "=r" (ret) : ); + return ret; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_sh_gcc.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_sh_gcc.h new file mode 100644 index 0000000..5ecc3b3 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_sh_gcc.h @@ -0,0 +1,36 @@ +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL +#define STACK_MAGIC 0 +#define REGS_TO_SAVE "r8", "r9", "r10", "r11", "r13", \ + "fr12", "fr13", "fr14", "fr15" + +// r12 Global context pointer, GP +// r14 Frame pointer, FP +// r15 Stack pointer, SP + +static int +slp_switch(void) +{ + int err; + void* fp; + int *stackref, stsizediff; + __asm__ volatile("" : : : REGS_TO_SAVE); + __asm__ volatile("mov.l r14, %0" : "=m"(fp) : :); + __asm__("mov r15, %0" : "=r"(stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile( + "add %0, r15\n" + "add %0, r14\n" + : /* no outputs */ + : "r"(stsizediff)); + SLP_RESTORE_STATE(); + __asm__ volatile("mov r0, %0" : "=r"(err) : :); + } + __asm__ volatile("mov.l %0, r14" : : "m"(fp) :); + __asm__ volatile("" : : : REGS_TO_SAVE); + return err; +} + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_sparc_sun_gcc.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_sparc_sun_gcc.h new file mode 100644 index 0000000..96990c3 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_sparc_sun_gcc.h @@ -0,0 +1,92 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 16-May-15 Alexey Borzenkov + * Move stack spilling code inside save/restore functions + * 30-Aug-13 Floris Bruynooghe + Clean the register windows again before returning. + This does not clobber the PIC register as it leaves + the current window intact and is required for multi- + threaded code to work correctly. + * 08-Mar-11 Floris Bruynooghe + * No need to set return value register explicitly + * before the stack and framepointer are adjusted + * as none of the other registers are influenced by + * this. Also don't needlessly clean the windows + * ('ta %0" :: "i" (ST_CLEAN_WINDOWS)') as that + * clobbers the gcc PIC register (%l7). + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * added support for SunOS sparc with gcc + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + + +#define STACK_MAGIC 0 + + +#if defined(__sparcv9) +#define SLP_FLUSHW __asm__ volatile ("flushw") +#else +#define SLP_FLUSHW __asm__ volatile ("ta 3") /* ST_FLUSH_WINDOWS */ +#endif + +/* On sparc we need to spill register windows inside save/restore functions */ +#define SLP_BEFORE_SAVE_STATE() SLP_FLUSHW +#define SLP_BEFORE_RESTORE_STATE() SLP_FLUSHW + + +static int +slp_switch(void) +{ + int err; + int *stackref, stsizediff; + + /* Put current stack pointer into stackref. + * Register spilling is done in save/restore. + */ + __asm__ volatile ("mov %%sp, %0" : "=r" (stackref)); + + { + /* Thou shalt put SLP_SAVE_STATE into a local block */ + /* Copy the current stack onto the heap */ + SLP_SAVE_STATE(stackref, stsizediff); + + /* Increment stack and frame pointer by stsizediff */ + __asm__ volatile ( + "add %0, %%sp, %%sp\n\t" + "add %0, %%fp, %%fp" + : : "r" (stsizediff)); + + /* Copy new stack from it's save store on the heap */ + SLP_RESTORE_STATE(); + + __asm__ volatile ("mov %1, %0" : "=r" (err) : "i" (0)); + return err; + } +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x32_unix.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x32_unix.h new file mode 100644 index 0000000..893369c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x32_unix.h @@ -0,0 +1,63 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 17-Aug-12 Fantix King + * Ported from amd64. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 0 + +#define REGS_TO_SAVE "r12", "r13", "r14", "r15" + + +static int +slp_switch(void) +{ + void* ebp; + void* ebx; + unsigned int csr; + unsigned short cw; + int err; + int *stackref, stsizediff; + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("fstcw %0" : "=m" (cw)); + __asm__ volatile ("stmxcsr %0" : "=m" (csr)); + __asm__ volatile ("movl %%ebp, %0" : "=m" (ebp)); + __asm__ volatile ("movl %%ebx, %0" : "=m" (ebx)); + __asm__ ("movl %%esp, %0" : "=g" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "addl %0, %%esp\n" + "addl %0, %%ebp\n" + : + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + } + __asm__ volatile ("movl %0, %%ebx" : : "m" (ebx)); + __asm__ volatile ("movl %0, %%ebp" : : "m" (ebp)); + __asm__ volatile ("ldmxcsr %0" : : "m" (csr)); + __asm__ volatile ("fldcw %0" : : "m" (cw)); + __asm__ volatile ("" : : : REGS_TO_SAVE); + __asm__ volatile ("xorl %%eax, %%eax" : "=a" (err)); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.asm b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.asm new file mode 100644 index 0000000..f5c72a2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.asm @@ -0,0 +1,111 @@ +; +; stack switching code for MASM on x641 +; Kristjan Valur Jonsson, sept 2005 +; + + +;prototypes for our calls +slp_save_state_asm PROTO +slp_restore_state_asm PROTO + + +pushxmm MACRO reg + sub rsp, 16 + .allocstack 16 + movaps [rsp], reg ; faster than movups, but we must be aligned + ; .savexmm128 reg, offset (don't know what offset is, no documentation) +ENDM +popxmm MACRO reg + movaps reg, [rsp] ; faster than movups, but we must be aligned + add rsp, 16 +ENDM + +pushreg MACRO reg + push reg + .pushreg reg +ENDM +popreg MACRO reg + pop reg +ENDM + + +.code +slp_switch PROC FRAME + ;realign stack to 16 bytes after return address push, makes the following faster + sub rsp,8 + .allocstack 8 + + pushxmm xmm15 + pushxmm xmm14 + pushxmm xmm13 + pushxmm xmm12 + pushxmm xmm11 + pushxmm xmm10 + pushxmm xmm9 + pushxmm xmm8 + pushxmm xmm7 + pushxmm xmm6 + + pushreg r15 + pushreg r14 + pushreg r13 + pushreg r12 + + pushreg rbp + pushreg rbx + pushreg rdi + pushreg rsi + + sub rsp, 10h ;allocate the singlefunction argument (must be multiple of 16) + .allocstack 10h +.endprolog + + lea rcx, [rsp+10h] ;load stack base that we are saving + call slp_save_state_asm ;pass stackpointer, return offset in eax + cmp rax, 1 + je EXIT1 + cmp rax, -1 + je EXIT2 + ;actual stack switch: + add rsp, rax + call slp_restore_state_asm + xor rax, rax ;return 0 + +EXIT: + + add rsp, 10h + popreg rsi + popreg rdi + popreg rbx + popreg rbp + + popreg r12 + popreg r13 + popreg r14 + popreg r15 + + popxmm xmm6 + popxmm xmm7 + popxmm xmm8 + popxmm xmm9 + popxmm xmm10 + popxmm xmm11 + popxmm xmm12 + popxmm xmm13 + popxmm xmm14 + popxmm xmm15 + + add rsp, 8 + ret + +EXIT1: + mov rax, 1 + jmp EXIT + +EXIT2: + sar rax, 1 + jmp EXIT + +slp_switch ENDP + +END \ No newline at end of file diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.obj b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.obj new file mode 100644 index 0000000000000000000000000000000000000000..64e3e6b898ec765d4e37075f7b1635ad24c9efa2 GIT binary patch literal 1078 zcmZ{j&ubG=5XWb`DJB@*%~BA=L%=;Gk}d_~52VO$4J4q2U~MY6&1RFl{E&?scGnt@ zn(9GNy!ihFEO@PV4?T&H9`x2*oO!!jlNJZwd!P4xlX&_;U$Bg3z>p zje>}2kp+MsxE|w5hOUr>YC~(=fz6fwPdZd5+Hlb^P3{;ZO@Yuv96GG&+Gx?QfclNd zhy2KN&~>fNnlHQRR;U1cMyQ?hlQ$~k<0KBbB<0uD2#PTjVo+na7Q;#m=@=3m;xJOa zs2V#)&Db`cY;WzTF9)11;SjkVQWE!?bPTC%x3h3^F2;aBns5!i%m4&-*h69;~AUpZR%rDpm!zuXY+kc zFCz-n*^4&c)5~}y3e?r-Evy*n*(lp9r%ti58Y#l5&)rDjx5EbRd}nC+_8znRzz&#& XZ_Fi+`GM=5Rl{n4%KxAK>jC@)Nz=zi literal 0 HcmV?d00001 diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_msvc.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_msvc.h new file mode 100644 index 0000000..601ea56 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x64_msvc.h @@ -0,0 +1,60 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 26-Sep-02 Christian Tismer + * again as a result of virtualized stack access, + * the compiler used less registers. Needed to + * explicit mention registers in order to get them saved. + * Thanks to Jeff Senn for pointing this out and help. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 01-Mar-02 Christian Tismer + * Initial final version after lots of iterations for i386. + */ + +/* Avoid alloca redefined warning on mingw64 */ +#ifndef alloca +#define alloca _alloca +#endif + +#define STACK_REFPLUS 1 +#define STACK_MAGIC 0 + +/* Use the generic support for an external assembly language slp_switch function. */ +#define EXTERNAL_ASM + +#ifdef SLP_EVAL +/* This always uses the external masm assembly file. */ +#endif + +/* + * further self-processing support + */ + +/* we have IsBadReadPtr available, so we can peek at objects */ +/* +#define STACKLESS_SPY + +#ifdef IMPLEMENT_STACKLESSMODULE +#include "Windows.h" +#define CANNOT_READ_MEM(p, bytes) IsBadReadPtr(p, bytes) + +static int IS_ON_STACK(void*p) +{ + int stackref; + intptr_t stackbase = ((intptr_t)&stackref) & 0xfffff000; + return (intptr_t)p >= stackbase && (intptr_t)p < stackbase + 0x00100000; +} + +#endif +*/ \ No newline at end of file diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x86_msvc.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x86_msvc.h new file mode 100644 index 0000000..0f3a59f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x86_msvc.h @@ -0,0 +1,326 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 26-Sep-02 Christian Tismer + * again as a result of virtualized stack access, + * the compiler used less registers. Needed to + * explicit mention registers in order to get them saved. + * Thanks to Jeff Senn for pointing this out and help. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for sparc + * 01-Mar-02 Christian Tismer + * Initial final version after lots of iterations for i386. + */ + +#define alloca _alloca + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +#define STACK_MAGIC 0 + +/* Some magic to quell warnings and keep slp_switch() from crashing when built + with VC90. Disable global optimizations, and the warning: frame pointer + register 'ebp' modified by inline assembly code. + + We used to just disable global optimizations ("g") but upstream stackless + Python, as well as stackman, turn off all optimizations. + +References: +https://github.com/stackless-dev/stackman/blob/dbc72fe5207a2055e658c819fdeab9731dee78b9/stackman/platforms/switch_x86_msvc.h +https://github.com/stackless-dev/stackless/blob/main-slp/Stackless/platf/switch_x86_msvc.h +*/ +#define WIN32_LEAN_AND_MEAN +#include + +#pragma optimize("", off) /* so that autos are stored on the stack */ +#pragma warning(disable:4731) +#pragma warning(disable:4733) /* disable warning about modifying FS[0] */ + +/** + * Most modern compilers and environments handle C++ exceptions without any + * special help from us. MSVC on 32-bit windows is an exception. There, C++ + * exceptions are dealt with using Windows' Structured Exception Handling + * (SEH). + * + * SEH is implemented as a singly linked list of nodes. The + * head of this list is stored in the Thread Information Block, which itself + * is pointed to from the FS register. It's the first field in the structure, + * or offset 0, so we can access it using assembly FS:[0], or the compiler + * intrinsics and field offset information from the headers (as we do below). + * Somewhat unusually, the tail of the list doesn't have prev == NULL, it has + * prev == 0xFFFFFFFF. + * + * SEH was designed for C, and traditionally uses the MSVC compiler + * intrinsincs __try{}/__except{}. It is also utilized for C++ exceptions by + * MSVC; there, every throw of a C++ exception raises a SEH error with the + * ExceptionCode 0xE06D7363; the SEH handler list is then traversed to + * deal with the exception. + * + * If the SEH list is corrupt, then when a C++ exception is thrown the program + * will abruptly exit with exit code 1. This does not use std::terminate(), so + * std::set_terminate() is useless to debug this. + * + * The SEH list is closely tied to the call stack; entering a function that + * uses __try{} or most C++ functions will push a new handler onto the front + * of the list. Returning from the function will remove the handler. Saving + * and restoring the head node of the SEH list (FS:[0]) per-greenlet is NOT + * ENOUGH to make SEH or exceptions work. + * + * Stack switching breaks SEH because the call stack no longer necessarily + * matches the SEH list. For example, given greenlet A that switches to + * greenlet B, at the moment of entering greenlet B, we will have any SEH + * handlers from greenlet A on the SEH list; greenlet B can then add its own + * handlers to the SEH list. When greenlet B switches back to greenlet A, + * greenlet B's handlers would still be on the SEH stack, but when switch() + * returns control to greenlet A, we have replaced the contents of the stack + * in memory, so all the address that greenlet B added to the SEH list are now + * invalid: part of the call stack has been unwound, but the SEH list was out + * of sync with the call stack. The net effect is that exception handling + * stops working. + * + * Thus, when switching greenlets, we need to be sure that the SEH list + * matches the effective call stack, "cutting out" any handlers that were + * pushed by the greenlet that switched out and which are no longer valid. + * + * The easiest way to do this is to capture the SEH list at the time the main + * greenlet for a thread is created, and, when initially starting a greenlet, + * start a new SEH list for it, which contains nothing but the handler + * established for the new greenlet itself, with the tail being the handlers + * for the main greenlet. If we then save and restore the SEH per-greenlet, + * they won't interfere with each others SEH lists. (No greenlet can unwind + * the call stack past the handlers established by the main greenlet). + * + * By observation, a new thread starts with three SEH handlers on the list. By + * the time we get around to creating the main greenlet, though, there can be + * many more, established by transient calls that lead to the creation of the + * main greenlet. Therefore, 3 is a magic constant telling us when to perform + * the initial slice. + * + * All of this can be debugged using a vectored exception handler, which + * operates independently of the SEH handler list, and is called first. + * Walking the SEH list at key points can also be helpful. + * + * References: + * https://en.wikipedia.org/wiki/Win32_Thread_Information_Block + * https://devblogs.microsoft.com/oldnewthing/20100730-00/?p=13273 + * https://docs.microsoft.com/en-us/cpp/cpp/try-except-statement?view=msvc-160 + * https://docs.microsoft.com/en-us/cpp/cpp/structured-exception-handling-c-cpp?view=msvc-160 + * https://docs.microsoft.com/en-us/windows/win32/debug/structured-exception-handling + * https://docs.microsoft.com/en-us/windows/win32/debug/using-a-vectored-exception-handler + * https://bytepointer.com/resources/pietrek_crash_course_depths_of_win32_seh.htm + */ +#define GREENLET_NEEDS_EXCEPTION_STATE_SAVED + + +typedef struct _GExceptionRegistration { + struct _GExceptionRegistration* prev; + void* handler_f; +} GExceptionRegistration; + +static void +slp_set_exception_state(const void *const seh_state) +{ + // Because the stack from from which we do this is ALSO a handler, and + // that one we want to keep, we need to relink the current SEH handler + // frame to point to this one, cutting out the middle men, as it were. + // + // Entering a try block doesn't change the SEH frame, but entering a + // function containing a try block does. + GExceptionRegistration* current_seh_state = (GExceptionRegistration*)__readfsdword(FIELD_OFFSET(NT_TIB, ExceptionList)); + current_seh_state->prev = (GExceptionRegistration*)seh_state; +} + + +static GExceptionRegistration* +x86_slp_get_third_oldest_handler() +{ + GExceptionRegistration* a = NULL; /* Closest to the top */ + GExceptionRegistration* b = NULL; /* second */ + GExceptionRegistration* c = NULL; + GExceptionRegistration* seh_state = (GExceptionRegistration*)__readfsdword(FIELD_OFFSET(NT_TIB, ExceptionList)); + a = b = c = seh_state; + + while (seh_state && seh_state != (GExceptionRegistration*)0xFFFFFFFF) { + if ((void*)seh_state->prev < (void*)100) { + fprintf(stderr, "\tERROR: Broken SEH chain.\n"); + return NULL; + } + a = b; + b = c; + c = seh_state; + + seh_state = seh_state->prev; + } + return a ? a : (b ? b : c); +} + + +static void* +slp_get_exception_state() +{ + // XXX: There appear to be three SEH handlers on the stack already at the + // start of the thread. Is that a guarantee? Almost certainly not. Yet in + // all observed cases it has been three. This is consistent with + // faulthandler off or on, and optimizations off or on. It may not be + // consistent with other operating system versions, though: we only have + // CI on one or two versions (don't ask what there are). + // In theory we could capture the number of handlers on the chain when + // PyInit__greenlet is called: there are probably only the default + // handlers at that point (unless we're embedded and people have used + // __try/__except or a C++ handler)? + return x86_slp_get_third_oldest_handler(); +} + +static int +slp_switch(void) +{ + /* MASM syntax is typically reversed from other assemblers. + It is usually + */ + int *stackref, stsizediff; + /* store the structured exception state for this stack */ + DWORD seh_state = __readfsdword(FIELD_OFFSET(NT_TIB, ExceptionList)); + __asm mov stackref, esp; + /* modify EBX, ESI and EDI in order to get them preserved */ + __asm mov ebx, ebx; + __asm xchg esi, edi; + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm { + mov eax, stsizediff + add esp, eax + add ebp, eax + } + SLP_RESTORE_STATE(); + } + __writefsdword(FIELD_OFFSET(NT_TIB, ExceptionList), seh_state); + return 0; +} + +/* re-enable ebp warning and global optimizations. */ +#pragma optimize("", on) +#pragma warning(default:4731) +#pragma warning(default:4733) /* disable warning about modifying FS[0] */ + + +#endif + +/* + * further self-processing support + */ + +/* we have IsBadReadPtr available, so we can peek at objects */ +#define STACKLESS_SPY + +#ifdef GREENLET_DEBUG + +#define CANNOT_READ_MEM(p, bytes) IsBadReadPtr(p, bytes) + +static int IS_ON_STACK(void*p) +{ + int stackref; + int stackbase = ((int)&stackref) & 0xfffff000; + return (int)p >= stackbase && (int)p < stackbase + 0x00100000; +} + +static void +x86_slp_show_seh_chain() +{ + GExceptionRegistration* seh_state = (GExceptionRegistration*)__readfsdword(FIELD_OFFSET(NT_TIB, ExceptionList)); + fprintf(stderr, "====== SEH Chain ======\n"); + while (seh_state && seh_state != (GExceptionRegistration*)0xFFFFFFFF) { + fprintf(stderr, "\tSEH_chain addr: %p handler: %p prev: %p\n", + seh_state, + seh_state->handler_f, seh_state->prev); + if ((void*)seh_state->prev < (void*)100) { + fprintf(stderr, "\tERROR: Broken chain.\n"); + break; + } + seh_state = seh_state->prev; + } + fprintf(stderr, "====== End SEH Chain ======\n"); + fflush(NULL); + return; +} + +//addVectoredExceptionHandler constants: +//CALL_FIRST means call this exception handler first; +//CALL_LAST means call this exception handler last +#define CALL_FIRST 1 +#define CALL_LAST 0 + +LONG WINAPI +GreenletVectorHandler(PEXCEPTION_POINTERS ExceptionInfo) +{ + // We get one of these for every C++ exception, with code + // E06D7363 + // This is a special value that means "C++ exception from MSVC" + // https://devblogs.microsoft.com/oldnewthing/20100730-00/?p=13273 + // + // Install in the module init function with: + // AddVectoredExceptionHandler(CALL_FIRST, GreenletVectorHandler); + PEXCEPTION_RECORD ExceptionRecord = ExceptionInfo->ExceptionRecord; + + fprintf(stderr, + "GOT VECTORED EXCEPTION:\n" + "\tExceptionCode : %p\n" + "\tExceptionFlags : %p\n" + "\tExceptionAddr : %p\n" + "\tNumberparams : %ld\n", + ExceptionRecord->ExceptionCode, + ExceptionRecord->ExceptionFlags, + ExceptionRecord->ExceptionAddress, + ExceptionRecord->NumberParameters + ); + if (ExceptionRecord->ExceptionFlags & 1) { + fprintf(stderr, "\t\tEH_NONCONTINUABLE\n" ); + } + if (ExceptionRecord->ExceptionFlags & 2) { + fprintf(stderr, "\t\tEH_UNWINDING\n" ); + } + if (ExceptionRecord->ExceptionFlags & 4) { + fprintf(stderr, "\t\tEH_EXIT_UNWIND\n" ); + } + if (ExceptionRecord->ExceptionFlags & 8) { + fprintf(stderr, "\t\tEH_STACK_INVALID\n" ); + } + if (ExceptionRecord->ExceptionFlags & 0x10) { + fprintf(stderr, "\t\tEH_NESTED_CALL\n" ); + } + if (ExceptionRecord->ExceptionFlags & 0x20) { + fprintf(stderr, "\t\tEH_TARGET_UNWIND\n" ); + } + if (ExceptionRecord->ExceptionFlags & 0x40) { + fprintf(stderr, "\t\tEH_COLLIDED_UNWIND\n" ); + } + fprintf(stderr, "\n"); + fflush(NULL); + for(DWORD i = 0; i < ExceptionRecord->NumberParameters; i++) { + fprintf(stderr, "\t\t\tParam %ld: %lX\n", i, ExceptionRecord->ExceptionInformation[i]); + } + + if (ExceptionRecord->NumberParameters == 3) { + fprintf(stderr, "\tAbout to traverse SEH chain\n"); + // C++ Exception records have 3 params. + x86_slp_show_seh_chain(); + } + + return EXCEPTION_CONTINUE_SEARCH; +} + + + + +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x86_unix.h b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x86_unix.h new file mode 100644 index 0000000..493fa6b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/platform/switch_x86_unix.h @@ -0,0 +1,105 @@ +/* + * this is the internal transfer function. + * + * HISTORY + * 3-May-13 Ralf Schmitt + * Add support for strange GCC caller-save decisions + * (ported from switch_aarch64_gcc.h) + * 19-Aug-11 Alexey Borzenkov + * Correctly save ebp, ebx and cw + * 07-Sep-05 (py-dev mailing list discussion) + * removed 'ebx' from the register-saved. !!!! WARNING !!!! + * It means that this file can no longer be compiled statically! + * It is now only suitable as part of a dynamic library! + * 24-Nov-02 Christian Tismer + * needed to add another magic constant to insure + * that f in slp_eval_frame(PyFrameObject *f) + * STACK_REFPLUS will probably be 1 in most cases. + * gets included into the saved stack area. + * 17-Sep-02 Christian Tismer + * after virtualizing stack save/restore, the + * stack size shrunk a bit. Needed to introduce + * an adjustment STACK_MAGIC per platform. + * 15-Sep-02 Gerd Woetzel + * slightly changed framework for spark + * 31-Avr-02 Armin Rigo + * Added ebx, esi and edi register-saves. + * 01-Mar-02 Samual M. Rushing + * Ported from i386. + */ + +#define STACK_REFPLUS 1 + +#ifdef SLP_EVAL + +/* #define STACK_MAGIC 3 */ +/* the above works fine with gcc 2.96, but 2.95.3 wants this */ +#define STACK_MAGIC 0 + +#if __GNUC__ > 4 || (__GNUC__ == 4 && __GNUC_MINOR__ >= 5) +# define ATTR_NOCLONE __attribute__((noclone)) +#else +# define ATTR_NOCLONE +#endif + +static int +slp_switch(void) +{ + int err; +#ifdef _WIN32 + void *seh; +#endif + void *ebp, *ebx; + unsigned short cw; + int *stackref, stsizediff; + __asm__ volatile ("" : : : "esi", "edi"); + __asm__ volatile ("fstcw %0" : "=m" (cw)); + __asm__ volatile ("movl %%ebp, %0" : "=m" (ebp)); + __asm__ volatile ("movl %%ebx, %0" : "=m" (ebx)); +#ifdef _WIN32 + __asm__ volatile ( + "movl %%fs:0x0, %%eax\n" + "movl %%eax, %0\n" + : "=m" (seh) + : + : "eax"); +#endif + __asm__ ("movl %%esp, %0" : "=g" (stackref)); + { + SLP_SAVE_STATE(stackref, stsizediff); + __asm__ volatile ( + "addl %0, %%esp\n" + "addl %0, %%ebp\n" + : + : "r" (stsizediff) + ); + SLP_RESTORE_STATE(); + __asm__ volatile ("xorl %%eax, %%eax" : "=a" (err)); + } +#ifdef _WIN32 + __asm__ volatile ( + "movl %0, %%eax\n" + "movl %%eax, %%fs:0x0\n" + : + : "m" (seh) + : "eax"); +#endif + __asm__ volatile ("movl %0, %%ebx" : : "m" (ebx)); + __asm__ volatile ("movl %0, %%ebp" : : "m" (ebp)); + __asm__ volatile ("fldcw %0" : : "m" (cw)); + __asm__ volatile ("" : : : "esi", "edi"); + return err; +} + +#endif + +/* + * further self-processing support + */ + +/* + * if you want to add self-inspection tools, place them + * here. See the x86_msvc for the necessary defines. + * These features are highly experimental und not + * essential yet. + */ diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/slp_platformselect.h b/netdeploy/lib/python3.11/site-packages/greenlet/slp_platformselect.h new file mode 100644 index 0000000..d9b7d0a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/slp_platformselect.h @@ -0,0 +1,77 @@ +/* + * Platform Selection for Stackless Python + */ +#ifdef __cplusplus +extern "C" { +#endif + +#if defined(MS_WIN32) && !defined(MS_WIN64) && defined(_M_IX86) && defined(_MSC_VER) +# include "platform/switch_x86_msvc.h" /* MS Visual Studio on X86 */ +#elif defined(MS_WIN64) && defined(_M_X64) && defined(_MSC_VER) || defined(__MINGW64__) +# include "platform/switch_x64_msvc.h" /* MS Visual Studio on X64 */ +#elif defined(MS_WIN64) && defined(_M_ARM64) +# include "platform/switch_arm64_msvc.h" /* MS Visual Studio on ARM64 */ +#elif defined(__GNUC__) && defined(__amd64__) && defined(__ILP32__) +# include "platform/switch_x32_unix.h" /* gcc on amd64 with x32 ABI */ +#elif defined(__GNUC__) && defined(__amd64__) +# include "platform/switch_amd64_unix.h" /* gcc on amd64 */ +#elif defined(__GNUC__) && defined(__i386__) +# include "platform/switch_x86_unix.h" /* gcc on X86 */ +#elif defined(__GNUC__) && defined(__powerpc64__) && (defined(__linux__) || defined(__FreeBSD__)) +# include "platform/switch_ppc64_linux.h" /* gcc on PowerPC 64-bit */ +#elif defined(__GNUC__) && defined(__PPC__) && (defined(__linux__) || defined(__FreeBSD__)) +# include "platform/switch_ppc_linux.h" /* gcc on PowerPC */ +#elif defined(__GNUC__) && defined(__POWERPC__) && defined(__APPLE__) +# include "platform/switch_ppc_macosx.h" /* Apple MacOS X on 32-bit PowerPC */ +#elif defined(__GNUC__) && defined(__powerpc64__) && defined(_AIX) +# include "platform/switch_ppc64_aix.h" /* gcc on AIX/PowerPC 64-bit */ +#elif defined(__GNUC__) && defined(_ARCH_PPC) && defined(_AIX) +# include "platform/switch_ppc_aix.h" /* gcc on AIX/PowerPC */ +#elif defined(__GNUC__) && defined(__powerpc__) && defined(__NetBSD__) +#include "platform/switch_ppc_unix.h" /* gcc on NetBSD/powerpc */ +#elif defined(__GNUC__) && defined(sparc) +# include "platform/switch_sparc_sun_gcc.h" /* SunOS sparc with gcc */ +#elif defined(__GNUC__) && defined(__sparc__) +# include "platform/switch_sparc_sun_gcc.h" /* NetBSD sparc with gcc */ +#elif defined(__SUNPRO_C) && defined(sparc) && defined(sun) +# include "platform/switch_sparc_sun_gcc.h" /* SunStudio on amd64 */ +#elif defined(__SUNPRO_C) && defined(__amd64__) && defined(sun) +# include "platform/switch_amd64_unix.h" /* SunStudio on amd64 */ +#elif defined(__SUNPRO_C) && defined(__i386__) && defined(sun) +# include "platform/switch_x86_unix.h" /* SunStudio on x86 */ +#elif defined(__GNUC__) && defined(__s390__) && defined(__linux__) +# include "platform/switch_s390_unix.h" /* Linux/S390 */ +#elif defined(__GNUC__) && defined(__s390x__) && defined(__linux__) +# include "platform/switch_s390_unix.h" /* Linux/S390 zSeries (64-bit) */ +#elif defined(__GNUC__) && defined(__arm__) +# ifdef __APPLE__ +# include +# endif +# if TARGET_OS_IPHONE +# include "platform/switch_arm32_ios.h" /* iPhone OS on arm32 */ +# else +# include "platform/switch_arm32_gcc.h" /* gcc using arm32 */ +# endif +#elif defined(__GNUC__) && defined(__mips__) && defined(__linux__) +# include "platform/switch_mips_unix.h" /* Linux/MIPS */ +#elif defined(__GNUC__) && defined(__aarch64__) +# include "platform/switch_aarch64_gcc.h" /* Aarch64 ABI */ +#elif defined(__GNUC__) && defined(__mc68000__) +# include "platform/switch_m68k_gcc.h" /* gcc on m68k */ +#elif defined(__GNUC__) && defined(__csky__) +#include "platform/switch_csky_gcc.h" /* gcc on csky */ +# elif defined(__GNUC__) && defined(__riscv) +# include "platform/switch_riscv_unix.h" /* gcc on RISC-V */ +#elif defined(__GNUC__) && defined(__alpha__) +# include "platform/switch_alpha_unix.h" /* gcc on DEC Alpha */ +#elif defined(MS_WIN32) && defined(__llvm__) && defined(__aarch64__) +# include "platform/switch_aarch64_gcc.h" /* LLVM Aarch64 ABI for Windows */ +#elif defined(__GNUC__) && defined(__loongarch64) && defined(__linux__) +# include "platform/switch_loongarch64_linux.h" /* LoongArch64 */ +#elif defined(__GNUC__) && defined(__sh__) +# include "platform/switch_sh_gcc.h" /* SuperH */ +#endif + +#ifdef __cplusplus +}; +#endif diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/__init__.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/__init__.py new file mode 100644 index 0000000..1861360 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/__init__.py @@ -0,0 +1,248 @@ +# -*- coding: utf-8 -*- +""" +Tests for greenlet. + +""" +import os +import sys +import sysconfig +import unittest + +from gc import collect +from gc import get_objects +from threading import active_count as active_thread_count +from time import sleep +from time import time + +import psutil + +from greenlet import greenlet as RawGreenlet +from greenlet import getcurrent + +from greenlet._greenlet import get_pending_cleanup_count +from greenlet._greenlet import get_total_main_greenlets + +from . import leakcheck + +PY312 = sys.version_info[:2] >= (3, 12) +PY313 = sys.version_info[:2] >= (3, 13) +# XXX: First tested on 3.14a7. Revisit all uses of this on later versions to ensure they +# are still valid. +PY314 = sys.version_info[:2] >= (3, 14) + +WIN = sys.platform.startswith("win") +RUNNING_ON_GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS') +RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') or RUNNING_ON_GITHUB_ACTIONS +RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR') +RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR +RUNNING_ON_MANYLINUX = os.environ.get('GREENLET_MANYLINUX') + +# Is the current interpreter free-threaded?) Note that this +# isn't the same as whether the GIL is enabled, this is the build-time +# value. Certain CPython details, like the garbage collector, +# work very differently on potentially-free-threaded builds than +# standard builds. +RUNNING_ON_FREETHREAD_BUILD = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) + +class TestCaseMetaClass(type): + # wrap each test method with + # a) leak checks + def __new__(cls, classname, bases, classDict): + # pylint and pep8 fight over what this should be called (mcs or cls). + # pylint gets it right, but we can't scope disable pep8, so we go with + # its convention. + # pylint: disable=bad-mcs-classmethod-argument + check_totalrefcount = True + + # Python 3: must copy, we mutate the classDict. Interestingly enough, + # it doesn't actually error out, but under 3.6 we wind up wrapping + # and re-wrapping the same items over and over and over. + for key, value in list(classDict.items()): + if key.startswith('test') and callable(value): + classDict.pop(key) + if check_totalrefcount: + value = leakcheck.wrap_refcount(value) + classDict[key] = value + return type.__new__(cls, classname, bases, classDict) + + +class TestCase(unittest.TestCase, metaclass=TestCaseMetaClass): + + cleanup_attempt_sleep_duration = 0.001 + cleanup_max_sleep_seconds = 1 + + def wait_for_pending_cleanups(self, + initial_active_threads=None, + initial_main_greenlets=None): + initial_active_threads = initial_active_threads or self.threads_before_test + initial_main_greenlets = initial_main_greenlets or self.main_greenlets_before_test + sleep_time = self.cleanup_attempt_sleep_duration + # NOTE: This is racy! A Python-level thread object may be dead + # and gone, but the C thread may not yet have fired its + # destructors and added to the queue. There's no particular + # way to know that's about to happen. We try to watch the + # Python threads to make sure they, at least, have gone away. + # Counting the main greenlets, which we can easily do deterministically, + # also helps. + + # Always sleep at least once to let other threads run + sleep(sleep_time) + quit_after = time() + self.cleanup_max_sleep_seconds + # TODO: We could add an API that calls us back when a particular main greenlet is deleted? + # It would have to drop the GIL + while ( + get_pending_cleanup_count() + or active_thread_count() > initial_active_threads + or (not self.expect_greenlet_leak + and get_total_main_greenlets() > initial_main_greenlets)): + sleep(sleep_time) + if time() > quit_after: + print("Time limit exceeded.") + print("Threads: Waiting for only", initial_active_threads, + "-->", active_thread_count()) + print("MGlets : Waiting for only", initial_main_greenlets, + "-->", get_total_main_greenlets()) + break + collect() + + def count_objects(self, kind=list, exact_kind=True): + # pylint:disable=unidiomatic-typecheck + # Collect the garbage. + for _ in range(3): + collect() + if exact_kind: + return sum( + 1 + for x in get_objects() + if type(x) is kind + ) + # instances + return sum( + 1 + for x in get_objects() + if isinstance(x, kind) + ) + + greenlets_before_test = 0 + threads_before_test = 0 + main_greenlets_before_test = 0 + expect_greenlet_leak = False + + def count_greenlets(self): + """ + Find all the greenlets and subclasses tracked by the GC. + """ + return self.count_objects(RawGreenlet, False) + + def setUp(self): + # Ensure the main greenlet exists, otherwise the first test + # gets a false positive leak + super().setUp() + getcurrent() + self.threads_before_test = active_thread_count() + self.main_greenlets_before_test = get_total_main_greenlets() + self.wait_for_pending_cleanups(self.threads_before_test, self.main_greenlets_before_test) + self.greenlets_before_test = self.count_greenlets() + + def tearDown(self): + if getattr(self, 'skipTearDown', False): + return + + self.wait_for_pending_cleanups(self.threads_before_test, self.main_greenlets_before_test) + super().tearDown() + + def get_expected_returncodes_for_aborted_process(self): + import signal + # The child should be aborted in an unusual way. On POSIX + # platforms, this is done with abort() and signal.SIGABRT, + # which is reflected in a negative return value; however, on + # Windows, even though we observe the child print "Fatal + # Python error: Aborted" and in older versions of the C + # runtime "This application has requested the Runtime to + # terminate it in an unusual way," it always has an exit code + # of 3. This is interesting because 3 is the error code for + # ERROR_PATH_NOT_FOUND; BUT: the C runtime abort() function + # also uses this code. + # + # If we link to the static C library on Windows, the error + # code changes to '0xc0000409' (hex(3221226505)), which + # apparently is STATUS_STACK_BUFFER_OVERRUN; but "What this + # means is that nowadays when you get a + # STATUS_STACK_BUFFER_OVERRUN, it doesn’t actually mean that + # there is a stack buffer overrun. It just means that the + # application decided to terminate itself with great haste." + # + # + # On windows, we've also seen '0xc0000005' (hex(3221225477)). + # That's "Access Violation" + # + # See + # https://devblogs.microsoft.com/oldnewthing/20110519-00/?p=10623 + # and + # https://docs.microsoft.com/en-us/previous-versions/k089yyh0(v=vs.140)?redirectedfrom=MSDN + # and + # https://devblogs.microsoft.com/oldnewthing/20190108-00/?p=100655 + expected_exit = ( + -signal.SIGABRT, + # But beginning on Python 3.11, the faulthandler + # that prints the C backtraces sometimes segfaults after + # reporting the exception but before printing the stack. + # This has only been seen on linux/gcc. + -signal.SIGSEGV, + ) if not WIN else ( + 3, + 0xc0000409, + 0xc0000005, + ) + return expected_exit + + def get_process_uss(self): + """ + Return the current process's USS in bytes. + + uss is available on Linux, macOS, Windows. Also known as + "Unique Set Size", this is the memory which is unique to a + process and which would be freed if the process was terminated + right now. + + If this is not supported by ``psutil``, this raises the + :exc:`unittest.SkipTest` exception. + """ + try: + return psutil.Process().memory_full_info().uss + except AttributeError as e: + raise unittest.SkipTest("uss not supported") from e + + def run_script(self, script_name, show_output=True): + import subprocess + script = os.path.join( + os.path.dirname(__file__), + script_name, + ) + + try: + return subprocess.check_output([sys.executable, script], + encoding='utf-8', + stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as ex: + if show_output: + print('-----') + print('Failed to run script', script) + print('~~~~~') + print(ex.output) + print('------') + raise + + + def assertScriptRaises(self, script_name, exitcodes=None): + import subprocess + with self.assertRaises(subprocess.CalledProcessError) as exc: + output = self.run_script(script_name, show_output=False) + __traceback_info__ = output + # We're going to fail the assertion if we get here, at least + # preserve the output in the traceback. + + if exitcodes is None: + exitcodes = self.get_expected_returncodes_for_aborted_process() + self.assertIn(exc.exception.returncode, exitcodes) + return exc.exception diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension.c b/netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension.c new file mode 100644 index 0000000..05e81c0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension.c @@ -0,0 +1,231 @@ +/* This is a set of functions used by test_extension_interface.py to test the + * Greenlet C API. + */ + +#include "../greenlet.h" + +#ifndef Py_RETURN_NONE +# define Py_RETURN_NONE return Py_INCREF(Py_None), Py_None +#endif + +#define TEST_MODULE_NAME "_test_extension" + +static PyObject* +test_switch(PyObject* self, PyObject* greenlet) +{ + PyObject* result = NULL; + + if (greenlet == NULL || !PyGreenlet_Check(greenlet)) { + PyErr_BadArgument(); + return NULL; + } + + result = PyGreenlet_Switch((PyGreenlet*)greenlet, NULL, NULL); + if (result == NULL) { + if (!PyErr_Occurred()) { + PyErr_SetString(PyExc_AssertionError, + "greenlet.switch() failed for some reason."); + } + return NULL; + } + Py_INCREF(result); + return result; +} + +static PyObject* +test_switch_kwargs(PyObject* self, PyObject* args, PyObject* kwargs) +{ + PyGreenlet* g = NULL; + PyObject* result = NULL; + + PyArg_ParseTuple(args, "O!", &PyGreenlet_Type, &g); + + if (g == NULL || !PyGreenlet_Check(g)) { + PyErr_BadArgument(); + return NULL; + } + + result = PyGreenlet_Switch(g, NULL, kwargs); + if (result == NULL) { + if (!PyErr_Occurred()) { + PyErr_SetString(PyExc_AssertionError, + "greenlet.switch() failed for some reason."); + } + return NULL; + } + Py_XINCREF(result); + return result; +} + +static PyObject* +test_getcurrent(PyObject* self) +{ + PyGreenlet* g = PyGreenlet_GetCurrent(); + if (g == NULL || !PyGreenlet_Check(g) || !PyGreenlet_ACTIVE(g)) { + PyErr_SetString(PyExc_AssertionError, + "getcurrent() returned an invalid greenlet"); + Py_XDECREF(g); + return NULL; + } + Py_DECREF(g); + Py_RETURN_NONE; +} + +static PyObject* +test_setparent(PyObject* self, PyObject* arg) +{ + PyGreenlet* current; + PyGreenlet* greenlet = NULL; + + if (arg == NULL || !PyGreenlet_Check(arg)) { + PyErr_BadArgument(); + return NULL; + } + if ((current = PyGreenlet_GetCurrent()) == NULL) { + return NULL; + } + greenlet = (PyGreenlet*)arg; + if (PyGreenlet_SetParent(greenlet, current)) { + Py_DECREF(current); + return NULL; + } + Py_DECREF(current); + if (PyGreenlet_Switch(greenlet, NULL, NULL) == NULL) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject* +test_new_greenlet(PyObject* self, PyObject* callable) +{ + PyObject* result = NULL; + PyGreenlet* greenlet = PyGreenlet_New(callable, NULL); + + if (!greenlet) { + return NULL; + } + + result = PyGreenlet_Switch(greenlet, NULL, NULL); + Py_CLEAR(greenlet); + if (result == NULL) { + return NULL; + } + + Py_INCREF(result); + return result; +} + +static PyObject* +test_raise_dead_greenlet(PyObject* self) +{ + PyErr_SetString(PyExc_GreenletExit, "test GreenletExit exception."); + return NULL; +} + +static PyObject* +test_raise_greenlet_error(PyObject* self) +{ + PyErr_SetString(PyExc_GreenletError, "test greenlet.error exception"); + return NULL; +} + +static PyObject* +test_throw(PyObject* self, PyGreenlet* g) +{ + const char msg[] = "take that sucka!"; + PyObject* msg_obj = Py_BuildValue("s", msg); + PyGreenlet_Throw(g, PyExc_ValueError, msg_obj, NULL); + Py_DECREF(msg_obj); + if (PyErr_Occurred()) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyObject* +test_throw_exact(PyObject* self, PyObject* args) +{ + PyGreenlet* g = NULL; + PyObject* typ = NULL; + PyObject* val = NULL; + PyObject* tb = NULL; + + if (!PyArg_ParseTuple(args, "OOOO:throw", &g, &typ, &val, &tb)) { + return NULL; + } + + PyGreenlet_Throw(g, typ, val, tb); + if (PyErr_Occurred()) { + return NULL; + } + Py_RETURN_NONE; +} + +static PyMethodDef test_methods[] = { + {"test_switch", + (PyCFunction)test_switch, + METH_O, + "Switch to the provided greenlet sending provided arguments, and \n" + "return the results."}, + {"test_switch_kwargs", + (PyCFunction)test_switch_kwargs, + METH_VARARGS | METH_KEYWORDS, + "Switch to the provided greenlet sending the provided keyword args."}, + {"test_getcurrent", + (PyCFunction)test_getcurrent, + METH_NOARGS, + "Test PyGreenlet_GetCurrent()"}, + {"test_setparent", + (PyCFunction)test_setparent, + METH_O, + "Se the parent of the provided greenlet and switch to it."}, + {"test_new_greenlet", + (PyCFunction)test_new_greenlet, + METH_O, + "Test PyGreenlet_New()"}, + {"test_raise_dead_greenlet", + (PyCFunction)test_raise_dead_greenlet, + METH_NOARGS, + "Just raise greenlet.GreenletExit"}, + {"test_raise_greenlet_error", + (PyCFunction)test_raise_greenlet_error, + METH_NOARGS, + "Just raise greenlet.error"}, + {"test_throw", + (PyCFunction)test_throw, + METH_O, + "Throw a ValueError at the provided greenlet"}, + {"test_throw_exact", + (PyCFunction)test_throw_exact, + METH_VARARGS, + "Throw exactly the arguments given at the provided greenlet"}, + {NULL, NULL, 0, NULL} +}; + + +#define INITERROR return NULL + +static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT, + TEST_MODULE_NAME, + NULL, + 0, + test_methods, + NULL, + NULL, + NULL, + NULL}; + +PyMODINIT_FUNC +PyInit__test_extension(void) +{ + PyObject* module = NULL; + module = PyModule_Create(&moduledef); + + if (module == NULL) { + return NULL; + } + + PyGreenlet_Import(); + return module; +} diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension.cpython-311-x86_64-linux-gnu.so b/netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..902c76adeb308ce0bdddbea24f6b8d6ba54cc1a9 GIT binary patch literal 17256 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 +#include + +struct exception_t { + int depth; + exception_t(int depth) : depth(depth) {} +}; + +/* Functions are called via pointers to prevent inlining */ +static void (*p_test_exception_throw_nonstd)(int depth); +static void (*p_test_exception_throw_std)(); +static PyObject* (*p_test_exception_switch_recurse)(int depth, int left); + +static void +test_exception_throw_nonstd(int depth) +{ + throw exception_t(depth); +} + +static void +test_exception_throw_std() +{ + throw std::runtime_error("Thrown from an extension."); +} + +static PyObject* +test_exception_switch_recurse(int depth, int left) +{ + if (left > 0) { + return p_test_exception_switch_recurse(depth, left - 1); + } + + PyObject* result = NULL; + PyGreenlet* self = PyGreenlet_GetCurrent(); + if (self == NULL) + return NULL; + + try { + if (PyGreenlet_Switch(PyGreenlet_GET_PARENT(self), NULL, NULL) == NULL) { + Py_DECREF(self); + return NULL; + } + p_test_exception_throw_nonstd(depth); + PyErr_SetString(PyExc_RuntimeError, + "throwing C++ exception didn't work"); + } + catch (const exception_t& e) { + if (e.depth != depth) + PyErr_SetString(PyExc_AssertionError, "depth mismatch"); + else + result = PyLong_FromLong(depth); + } + catch (...) { + PyErr_SetString(PyExc_RuntimeError, "unexpected C++ exception"); + } + + Py_DECREF(self); + return result; +} + +/* test_exception_switch(int depth) + * - recurses depth times + * - switches to parent inside try/catch block + * - throws an exception that (expected to be caught in the same function) + * - verifies depth matches (exceptions shouldn't be caught in other greenlets) + */ +static PyObject* +test_exception_switch(PyObject* UNUSED(self), PyObject* args) +{ + int depth; + if (!PyArg_ParseTuple(args, "i", &depth)) + return NULL; + return p_test_exception_switch_recurse(depth, depth); +} + + +static PyObject* +py_test_exception_throw_nonstd(PyObject* self, PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) + return NULL; + p_test_exception_throw_nonstd(0); + PyErr_SetString(PyExc_AssertionError, "unreachable code running after throw"); + return NULL; +} + +static PyObject* +py_test_exception_throw_std(PyObject* self, PyObject* args) +{ + if (!PyArg_ParseTuple(args, "")) + return NULL; + p_test_exception_throw_std(); + PyErr_SetString(PyExc_AssertionError, "unreachable code running after throw"); + return NULL; +} + +static PyObject* +py_test_call(PyObject* self, PyObject* arg) +{ + PyObject* noargs = PyTuple_New(0); + PyObject* ret = PyObject_Call(arg, noargs, nullptr); + Py_DECREF(noargs); + return ret; +} + + + +/* test_exception_switch_and_do_in_g2(g2func) + * - creates new greenlet g2 to run g2func + * - switches to g2 inside try/catch block + * - verifies that no exception has been caught + * + * it is used together with test_exception_throw to verify that unhandled + * exceptions thrown in one greenlet do not propagate to other greenlet nor + * segfault the process. + */ +static PyObject* +test_exception_switch_and_do_in_g2(PyObject* self, PyObject* args) +{ + PyObject* g2func = NULL; + PyObject* result = NULL; + + if (!PyArg_ParseTuple(args, "O", &g2func)) + return NULL; + PyGreenlet* g2 = PyGreenlet_New(g2func, NULL); + if (!g2) { + return NULL; + } + + try { + result = PyGreenlet_Switch(g2, NULL, NULL); + if (!result) { + return NULL; + } + } + catch (const exception_t& e) { + /* if we are here the memory can be already corrupted and the program + * might crash before below py-level exception might become printed. + * -> print something to stderr to make it clear that we had entered + * this catch block. + * See comments in inner_bootstrap() + */ +#if defined(WIN32) || defined(_WIN32) + fprintf(stderr, "C++ exception unexpectedly caught in g1\n"); + PyErr_SetString(PyExc_AssertionError, "C++ exception unexpectedly caught in g1"); + Py_XDECREF(result); + return NULL; +#else + throw; +#endif + } + + Py_XDECREF(result); + Py_RETURN_NONE; +} + +static PyMethodDef test_methods[] = { + {"test_exception_switch", + (PyCFunction)&test_exception_switch, + METH_VARARGS, + "Switches to parent twice, to test exception handling and greenlet " + "switching."}, + {"test_exception_switch_and_do_in_g2", + (PyCFunction)&test_exception_switch_and_do_in_g2, + METH_VARARGS, + "Creates new greenlet g2 to run g2func and switches to it inside try/catch " + "block. Used together with test_exception_throw to verify that unhandled " + "C++ exceptions thrown in a greenlet doe not corrupt memory."}, + {"test_exception_throw_nonstd", + (PyCFunction)&py_test_exception_throw_nonstd, + METH_VARARGS, + "Throws non-standard C++ exception. Calling this function directly should abort the process." + }, + {"test_exception_throw_std", + (PyCFunction)&py_test_exception_throw_std, + METH_VARARGS, + "Throws standard C++ exception. Calling this function directly should abort the process." + }, + {"test_call", + (PyCFunction)&py_test_call, + METH_O, + "Call the given callable. Unlike calling it directly, this creates a " + "new C-level stack frame, which may be helpful in testing." + }, + {NULL, NULL, 0, NULL} +}; + + +static struct PyModuleDef moduledef = {PyModuleDef_HEAD_INIT, + "greenlet.tests._test_extension_cpp", + NULL, + 0, + test_methods, + NULL, + NULL, + NULL, + NULL}; + +PyMODINIT_FUNC +PyInit__test_extension_cpp(void) +{ + PyObject* module = NULL; + + module = PyModule_Create(&moduledef); + + if (module == NULL) { + return NULL; + } + + PyGreenlet_Import(); + if (_PyGreenlet_API == NULL) { + return NULL; + } + + p_test_exception_throw_nonstd = test_exception_throw_nonstd; + p_test_exception_throw_std = test_exception_throw_std; + p_test_exception_switch_recurse = test_exception_switch_recurse; + + return module; +} diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpython-311-x86_64-linux-gnu.so b/netdeploy/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpython-311-x86_64-linux-gnu.so new file mode 100755 index 0000000000000000000000000000000000000000..ce8c5515c31d4a964bda6c42fcb4f512f43c2f5e GIT binary patch literal 57920 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%0Ds! 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 appearing to return, even though +# it has yet to run. +print('In main with', x, flush=True) +g.switch() +print('RESULTS', results) diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_cpp_exception.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_cpp_exception.py new file mode 100644 index 0000000..fa4dc2e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_cpp_exception.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +""" +Helper for testing a C++ exception throw aborts the process. + +Takes one argument, the name of the function in :mod:`_test_extension_cpp` to call. +""" +import sys +import greenlet +from greenlet.tests import _test_extension_cpp +print('fail_cpp_exception is running') + +def run_unhandled_exception_in_greenlet_aborts(): + def _(): + _test_extension_cpp.test_exception_switch_and_do_in_g2( + _test_extension_cpp.test_exception_throw_nonstd + ) + g1 = greenlet.greenlet(_) + g1.switch() + + +func_name = sys.argv[1] +try: + func = getattr(_test_extension_cpp, func_name) +except AttributeError: + if func_name == run_unhandled_exception_in_greenlet_aborts.__name__: + func = run_unhandled_exception_in_greenlet_aborts + elif func_name == 'run_as_greenlet_target': + g = greenlet.greenlet(_test_extension_cpp.test_exception_throw_std) + func = g.switch + else: + raise +print('raising', func, flush=True) +func() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_initialstub_already_started.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_initialstub_already_started.py new file mode 100644 index 0000000..c1a44ef --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_initialstub_already_started.py @@ -0,0 +1,78 @@ +""" +Testing initialstub throwing an already started exception. +""" + +import greenlet + +a = None +b = None +c = None +main = greenlet.getcurrent() + +# If we switch into a dead greenlet, +# we go looking for its parents. +# if a parent is not yet started, we start it. + +results = [] + +def a_run(*args): + #results.append('A') + results.append(('Begin A', args)) + + +def c_run(): + results.append('Begin C') + b.switch('From C') + results.append('C done') + +class A(greenlet.greenlet): pass + +class B(greenlet.greenlet): + doing_it = False + def __getattribute__(self, name): + if name == 'run' and not self.doing_it: + assert greenlet.getcurrent() is c + self.doing_it = True + results.append('Switch to b from B.__getattribute__ in ' + + type(greenlet.getcurrent()).__name__) + b.switch() + results.append('B.__getattribute__ back from main in ' + + type(greenlet.getcurrent()).__name__) + if name == 'run': + name = '_B_run' + return object.__getattribute__(self, name) + + def _B_run(self, *arg): + results.append(('Begin B', arg)) + results.append('_B_run switching to main') + main.switch('From B') + +class C(greenlet.greenlet): + pass +a = A(a_run) +b = B(parent=a) +c = C(c_run, b) + +# Start a child; while running, it will start B, +# but starting B will ALSO start B. +result = c.switch() +results.append(('main from c', result)) + +# Switch back to C, which was in the middle of switching +# already. This will throw the ``GreenletStartedWhileInPython`` +# exception, which results in parent A getting started (B is finished) +c.switch() + +results.append(('A dead?', a.dead, 'B dead?', b.dead, 'C dead?', c.dead)) + +# A and B should both be dead now. +assert a.dead +assert b.dead +assert not c.dead + +result = c.switch() +results.append(('main from c.2', result)) +# Now C is dead +assert c.dead + +print("RESULTS:", results) diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_slp_switch.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_slp_switch.py new file mode 100644 index 0000000..0990526 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_slp_switch.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +""" +A test helper for seeing what happens when slp_switch() +fails. +""" +# pragma: no cover + +import greenlet + + +print('fail_slp_switch is running', flush=True) + +runs = [] +def func(): + runs.append(1) + greenlet.getcurrent().parent.switch() + runs.append(2) + greenlet.getcurrent().parent.switch() + runs.append(3) + +g = greenlet._greenlet.UnswitchableGreenlet(func) +g.switch() +assert runs == [1] +g.switch() +assert runs == [1, 2] +g.force_slp_switch_error = True + +# This should crash. +g.switch() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets.py new file mode 100644 index 0000000..e151b19 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets.py @@ -0,0 +1,44 @@ +""" +Uses a trace function to switch greenlets at unexpected times. + +In the trace function, we switch from the current greenlet to another +greenlet, which switches +""" +import greenlet + +g1 = None +g2 = None + +switch_to_g2 = False + +def tracefunc(*args): + print('TRACE', *args) + global switch_to_g2 + if switch_to_g2: + switch_to_g2 = False + g2.switch() + print('\tLEAVE TRACE', *args) + +def g1_run(): + print('In g1_run') + global switch_to_g2 + switch_to_g2 = True + from_parent = greenlet.getcurrent().parent.switch() + print('Return to g1_run') + print('From parent', from_parent) + +def g2_run(): + #g1.switch() + greenlet.getcurrent().parent.switch() + +greenlet.settrace(tracefunc) + +g1 = greenlet.greenlet(g1_run) +g2 = greenlet.greenlet(g2_run) + +# This switch didn't actually finish! +# And if it did, it would raise TypeError +# because g1_run() doesn't take any arguments. +g1.switch(1) +print('Back in main') +g1.switch(2) diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets2.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets2.py new file mode 100644 index 0000000..1f6b66b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets2.py @@ -0,0 +1,55 @@ +""" +Like fail_switch_three_greenlets, but the call into g1_run would actually be +valid. +""" +import greenlet + +g1 = None +g2 = None + +switch_to_g2 = True + +results = [] + +def tracefunc(*args): + results.append(('trace', args[0])) + print('TRACE', *args) + global switch_to_g2 + if switch_to_g2: + switch_to_g2 = False + g2.switch('g2 from tracefunc') + print('\tLEAVE TRACE', *args) + +def g1_run(arg): + results.append(('g1 arg', arg)) + print('In g1_run') + from_parent = greenlet.getcurrent().parent.switch('from g1_run') + results.append(('g1 from parent', from_parent)) + return 'g1 done' + +def g2_run(arg): + #g1.switch() + results.append(('g2 arg', arg)) + parent = greenlet.getcurrent().parent.switch('from g2_run') + global switch_to_g2 + switch_to_g2 = False + results.append(('g2 from parent', parent)) + return 'g2 done' + + +greenlet.settrace(tracefunc) + +g1 = greenlet.greenlet(g1_run) +g2 = greenlet.greenlet(g2_run) + +x = g1.switch('g1 from main') +results.append(('main g1', x)) +print('Back in main', x) +x = g1.switch('g2 from main') +results.append(('main g2', x)) +print('back in amain again', x) +x = g1.switch('g1 from main 2') +results.append(('main g1.2', x)) +x = g2.switch() +results.append(('main g2.2', x)) +print("RESULTS:", results) diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_two_greenlets.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_two_greenlets.py new file mode 100644 index 0000000..3e52345 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/fail_switch_two_greenlets.py @@ -0,0 +1,41 @@ +""" +Uses a trace function to switch greenlets at unexpected times. + +In the trace function, we switch from the current greenlet to another +greenlet, which switches +""" +import greenlet + +g1 = None +g2 = None + +switch_to_g2 = False + +def tracefunc(*args): + print('TRACE', *args) + global switch_to_g2 + if switch_to_g2: + switch_to_g2 = False + g2.switch() + print('\tLEAVE TRACE', *args) + +def g1_run(): + print('In g1_run') + global switch_to_g2 + switch_to_g2 = True + greenlet.getcurrent().parent.switch() + print('Return to g1_run') + print('Falling off end of g1_run') + +def g2_run(): + g1.switch() + print('Falling off end of g2') + +greenlet.settrace(tracefunc) + +g1 = greenlet.greenlet(g1_run) +g2 = greenlet.greenlet(g2_run) + +g1.switch() +print('Falling off end of main') +g2.switch() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/leakcheck.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/leakcheck.py new file mode 100644 index 0000000..7046e41 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/leakcheck.py @@ -0,0 +1,336 @@ +# Copyright (c) 2018 gevent community +# Copyright (c) 2021 greenlet community +# +# This was originally part of gevent's test suite. The main author +# (Jason Madden) vendored a copy of it into greenlet. +# +# 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. +from __future__ import print_function + +import os +import sys +import gc + +from functools import wraps +import unittest + + +import objgraph + +# graphviz 0.18 (Nov 7 2021), available only on Python 3.6 and newer, +# has added type hints (sigh). It wants to use ``typing.Literal`` for +# some stuff, but that's only available on Python 3.9+. If that's not +# found, it creates a ``unittest.mock.MagicMock`` object and annotates +# with that. These are GC'able objects, and doing almost *anything* +# with them results in an explosion of objects. For example, trying to +# compare them for equality creates new objects. This causes our +# leakchecks to fail, with reports like: +# +# greenlet.tests.leakcheck.LeakCheckError: refcount increased by [337, 1333, 343, 430, 530, 643, 769] +# _Call 1820 +546 +# dict 4094 +76 +# MagicProxy 585 +73 +# tuple 2693 +66 +# _CallList 24 +3 +# weakref 1441 +1 +# function 5996 +1 +# type 736 +1 +# cell 592 +1 +# MagicMock 8 +1 +# +# To avoid this, we *could* filter this type of object out early. In +# principle it could leak, but we don't use mocks in greenlet, so it +# doesn't leak from us. However, a further issue is that ``MagicMock`` +# objects have subobjects that are also GC'able, like ``_Call``, and +# those create new mocks of their own too. So we'd have to filter them +# as well, and they're not public. That's OK, we can workaround the +# problem by being very careful to never compare by equality or other +# user-defined operators, only using object identity or other builtin +# functions. + +RUNNING_ON_GITHUB_ACTIONS = os.environ.get('GITHUB_ACTIONS') +RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') or RUNNING_ON_GITHUB_ACTIONS +RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR') +RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR +RUNNING_ON_MANYLINUX = os.environ.get('GREENLET_MANYLINUX') +SKIP_LEAKCHECKS = RUNNING_ON_MANYLINUX or os.environ.get('GREENLET_SKIP_LEAKCHECKS') +SKIP_FAILING_LEAKCHECKS = os.environ.get('GREENLET_SKIP_FAILING_LEAKCHECKS') +ONLY_FAILING_LEAKCHECKS = os.environ.get('GREENLET_ONLY_FAILING_LEAKCHECKS') + +def ignores_leakcheck(func): + """ + Ignore the given object during leakchecks. + + Can be applied to a method, in which case the method will run, but + will not be subject to leak checks. + + If applied to a class, the entire class will be skipped during leakchecks. This + is intended to be used for classes that are very slow and cause problems such as + test timeouts; typically it will be used for classes that are subclasses of a base + class and specify variants of behaviour (such as pool sizes). + """ + func.ignore_leakcheck = True + return func + +def fails_leakcheck(func): + """ + Mark that the function is known to leak. + """ + func.fails_leakcheck = True + if SKIP_FAILING_LEAKCHECKS: + func = unittest.skip("Skipping known failures")(func) + return func + +class LeakCheckError(AssertionError): + pass + +if hasattr(sys, 'getobjects'): + # In a Python build with ``--with-trace-refs``, make objgraph + # trace *all* the objects, not just those that are tracked by the + # GC + class _MockGC(object): + def get_objects(self): + return sys.getobjects(0) # pylint:disable=no-member + def __getattr__(self, name): + return getattr(gc, name) + objgraph.gc = _MockGC() + fails_strict_leakcheck = fails_leakcheck +else: + def fails_strict_leakcheck(func): + """ + Decorator for a function that is known to fail when running + strict (``sys.getobjects()``) leakchecks. + + This type of leakcheck finds all objects, even those, such as + strings, which are not tracked by the garbage collector. + """ + return func + +class ignores_types_in_strict_leakcheck(object): + def __init__(self, types): + self.types = types + def __call__(self, func): + func.leakcheck_ignore_types = self.types + return func + +class _RefCountChecker(object): + + # Some builtin things that we ignore + # XXX: Those things were ignored by gevent, but they're important here, + # presumably. + IGNORED_TYPES = () #(tuple, dict, types.FrameType, types.TracebackType) + + # Names of types that should be ignored. Use this when we cannot + # or don't want to import the class directly. + IGNORED_TYPE_NAMES = ( + # This appears in Python3.14 with the JIT enabled. It + # doesn't seem to be directly exposed to Python; the only way to get + # one is to cause code to get jitted and then look for all objects + # and find one with this name. But they multiply as code + # executes and gets jitted, in ways we don't want to rely on. + # So just ignore it. + 'uop_executor', + ) + + def __init__(self, testcase, function): + self.testcase = testcase + self.function = function + self.deltas = [] + self.peak_stats = {} + self.ignored_types = () + + # The very first time we are called, we have already been + # self.setUp() by the test runner, so we don't need to do it again. + self.needs_setUp = False + + def _include_object_p(self, obj): + # pylint:disable=too-many-return-statements + # + # See the comment block at the top. We must be careful to + # avoid invoking user-defined operations. + if obj is self: + return False + kind = type(obj) + # ``self._include_object_p == obj`` returns NotImplemented + # for non-function objects, which causes the interpreter + # to try to reverse the order of arguments...which leads + # to the explosion of mock objects. We don't want that, so we implement + # the check manually. + if kind == type(self._include_object_p): + try: + # pylint:disable=not-callable + exact_method_equals = self._include_object_p.__eq__(obj) + except AttributeError: + # Python 2.7 methods may only have __cmp__, and that raises a + # TypeError for non-method arguments + # pylint:disable=no-member + exact_method_equals = self._include_object_p.__cmp__(obj) == 0 + + if exact_method_equals is not NotImplemented and exact_method_equals: + return False + + # Similarly, we need to check identity in our __dict__ to avoid mock explosions. + for x in self.__dict__.values(): + if obj is x: + return False + + + if ( + kind in self.ignored_types + or kind in self.IGNORED_TYPES + or kind.__name__ in self.IGNORED_TYPE_NAMES + ): + return False + + + return True + + def _growth(self): + return objgraph.growth(limit=None, peak_stats=self.peak_stats, + filter=self._include_object_p) + + def _report_diff(self, growth): + if not growth: + return "" + + lines = [] + width = max(len(name) for name, _, _ in growth) + for name, count, delta in growth: + lines.append('%-*s%9d %+9d' % (width, name, count, delta)) + + diff = '\n'.join(lines) + return diff + + + def _run_test(self, args, kwargs): + gc_enabled = gc.isenabled() + gc.disable() + + if self.needs_setUp: + self.testcase.setUp() + self.testcase.skipTearDown = False + try: + self.function(self.testcase, *args, **kwargs) + finally: + self.testcase.tearDown() + self.testcase.doCleanups() + self.testcase.skipTearDown = True + self.needs_setUp = True + if gc_enabled: + gc.enable() + + def _growth_after(self): + # Grab post snapshot + # pylint:disable=no-member + if 'urlparse' in sys.modules: + sys.modules['urlparse'].clear_cache() + if 'urllib.parse' in sys.modules: + sys.modules['urllib.parse'].clear_cache() + + return self._growth() + + def _check_deltas(self, growth): + # Return false when we have decided there is no leak, + # true if we should keep looping, raises an assertion + # if we have decided there is a leak. + + deltas = self.deltas + if not deltas: + # We haven't run yet, no data, keep looping + return True + + if gc.garbage: + raise LeakCheckError("Generated uncollectable garbage %r" % (gc.garbage,)) + + + # the following configurations are classified as "no leak" + # [0, 0] + # [x, 0, 0] + # [... a, b, c, d] where a+b+c+d = 0 + # + # the following configurations are classified as "leak" + # [... z, z, z] where z > 0 + + if deltas[-2:] == [0, 0] and len(deltas) in (2, 3): + return False + + if deltas[-3:] == [0, 0, 0]: + return False + + if len(deltas) >= 4 and sum(deltas[-4:]) == 0: + return False + + if len(deltas) >= 3 and deltas[-1] > 0 and deltas[-1] == deltas[-2] and deltas[-2] == deltas[-3]: + diff = self._report_diff(growth) + raise LeakCheckError('refcount increased by %r\n%s' % (deltas, diff)) + + # OK, we don't know for sure yet. Let's search for more + if sum(deltas[-3:]) <= 0 or sum(deltas[-4:]) <= 0 or deltas[-4:].count(0) >= 2: + # this is suspicious, so give a few more runs + limit = 11 + else: + limit = 7 + if len(deltas) >= limit: + raise LeakCheckError('refcount increased by %r\n%s' + % (deltas, + self._report_diff(growth))) + + # We couldn't decide yet, keep going + return True + + def __call__(self, args, kwargs): + for _ in range(3): + gc.collect() + + expect_failure = getattr(self.function, 'fails_leakcheck', False) + if expect_failure: + self.testcase.expect_greenlet_leak = True + self.ignored_types = getattr(self.function, "leakcheck_ignore_types", ()) + + # Capture state before; the incremental will be + # updated by each call to _growth_after + growth = self._growth() + + try: + while self._check_deltas(growth): + self._run_test(args, kwargs) + + growth = self._growth_after() + + self.deltas.append(sum((stat[2] for stat in growth))) + except LeakCheckError: + if not expect_failure: + raise + else: + if expect_failure: + raise LeakCheckError("Expected %s to leak but it did not." % (self.function,)) + +def wrap_refcount(method): + if getattr(method, 'ignore_leakcheck', False) or SKIP_LEAKCHECKS: + return method + + @wraps(method) + def wrapper(self, *args, **kwargs): # pylint:disable=too-many-branches + if getattr(self, 'ignore_leakcheck', False): + raise unittest.SkipTest("This class ignored during leakchecks") + if ONLY_FAILING_LEAKCHECKS and not getattr(method, 'fails_leakcheck', False): + raise unittest.SkipTest("Only running tests that fail leakchecks.") + return _RefCountChecker(self, method)(args, kwargs) + + return wrapper diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_contextvars.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_contextvars.py new file mode 100644 index 0000000..b0d1ccf --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_contextvars.py @@ -0,0 +1,312 @@ +from __future__ import print_function + +import gc +import sys +import unittest + +from functools import partial +from unittest import skipUnless +from unittest import skipIf + +from greenlet import greenlet +from greenlet import getcurrent +from . import TestCase +from . import PY314 + +try: + from contextvars import Context + from contextvars import ContextVar + from contextvars import copy_context + # From the documentation: + # + # Important: Context Variables should be created at the top module + # level and never in closures. Context objects hold strong + # references to context variables which prevents context variables + # from being properly garbage collected. + ID_VAR = ContextVar("id", default=None) + VAR_VAR = ContextVar("var", default=None) + ContextVar = None +except ImportError: + Context = ContextVar = copy_context = None + +# We don't support testing if greenlet's built-in context var support is disabled. +@skipUnless(Context is not None, "ContextVar not supported") +class ContextVarsTests(TestCase): + def _new_ctx_run(self, *args, **kwargs): + return copy_context().run(*args, **kwargs) + + def _increment(self, greenlet_id, callback, counts, expect): + ctx_var = ID_VAR + if expect is None: + self.assertIsNone(ctx_var.get()) + else: + self.assertEqual(ctx_var.get(), expect) + ctx_var.set(greenlet_id) + for _ in range(2): + counts[ctx_var.get()] += 1 + callback() + + def _test_context(self, propagate_by): + # pylint:disable=too-many-branches + ID_VAR.set(0) + + callback = getcurrent().switch + counts = dict((i, 0) for i in range(5)) + + lets = [ + greenlet(partial( + partial( + copy_context().run, + self._increment + ) if propagate_by == "run" else self._increment, + greenlet_id=i, + callback=callback, + counts=counts, + expect=( + i - 1 if propagate_by == "share" else + 0 if propagate_by in ("set", "run") else None + ) + )) + for i in range(1, 5) + ] + + for let in lets: + if propagate_by == "set": + let.gr_context = copy_context() + elif propagate_by == "share": + let.gr_context = getcurrent().gr_context + + for i in range(2): + counts[ID_VAR.get()] += 1 + for let in lets: + let.switch() + + if propagate_by == "run": + # Must leave each context.run() in reverse order of entry + for let in reversed(lets): + let.switch() + else: + # No context.run(), so fine to exit in any order. + for let in lets: + let.switch() + + for let in lets: + self.assertTrue(let.dead) + # When using run(), we leave the run() as the greenlet dies, + # and there's no context "underneath". When not using run(), + # gr_context still reflects the context the greenlet was + # running in. + if propagate_by == 'run': + self.assertIsNone(let.gr_context) + else: + self.assertIsNotNone(let.gr_context) + + + if propagate_by == "share": + self.assertEqual(counts, {0: 1, 1: 1, 2: 1, 3: 1, 4: 6}) + else: + self.assertEqual(set(counts.values()), set([2])) + + def test_context_propagated_by_context_run(self): + self._new_ctx_run(self._test_context, "run") + + def test_context_propagated_by_setting_attribute(self): + self._new_ctx_run(self._test_context, "set") + + def test_context_not_propagated(self): + self._new_ctx_run(self._test_context, None) + + def test_context_shared(self): + self._new_ctx_run(self._test_context, "share") + + def test_break_ctxvars(self): + let1 = greenlet(copy_context().run) + let2 = greenlet(copy_context().run) + let1.switch(getcurrent().switch) + let2.switch(getcurrent().switch) + # Since let2 entered the current context and let1 exits its own, the + # interpreter emits: + # RuntimeError: cannot exit context: thread state references a different context object + let1.switch() + + def test_not_broken_if_using_attribute_instead_of_context_run(self): + let1 = greenlet(getcurrent().switch) + let2 = greenlet(getcurrent().switch) + let1.gr_context = copy_context() + let2.gr_context = copy_context() + let1.switch() + let2.switch() + let1.switch() + let2.switch() + + def test_context_assignment_while_running(self): + # pylint:disable=too-many-statements + ID_VAR.set(None) + + def target(): + self.assertIsNone(ID_VAR.get()) + self.assertIsNone(gr.gr_context) + + # Context is created on first use + ID_VAR.set(1) + self.assertIsInstance(gr.gr_context, Context) + self.assertEqual(ID_VAR.get(), 1) + self.assertEqual(gr.gr_context[ID_VAR], 1) + + # Clearing the context makes it get re-created as another + # empty context when next used + old_context = gr.gr_context + gr.gr_context = None # assign None while running + self.assertIsNone(ID_VAR.get()) + self.assertIsNone(gr.gr_context) + ID_VAR.set(2) + self.assertIsInstance(gr.gr_context, Context) + self.assertEqual(ID_VAR.get(), 2) + self.assertEqual(gr.gr_context[ID_VAR], 2) + + new_context = gr.gr_context + getcurrent().parent.switch((old_context, new_context)) + # parent switches us back to old_context + + self.assertEqual(ID_VAR.get(), 1) + gr.gr_context = new_context # assign non-None while running + self.assertEqual(ID_VAR.get(), 2) + + getcurrent().parent.switch() + # parent switches us back to no context + self.assertIsNone(ID_VAR.get()) + self.assertIsNone(gr.gr_context) + gr.gr_context = old_context + self.assertEqual(ID_VAR.get(), 1) + + getcurrent().parent.switch() + # parent switches us back to no context + self.assertIsNone(ID_VAR.get()) + self.assertIsNone(gr.gr_context) + + gr = greenlet(target) + + with self.assertRaisesRegex(AttributeError, "can't delete context attribute"): + del gr.gr_context + + self.assertIsNone(gr.gr_context) + old_context, new_context = gr.switch() + self.assertIs(new_context, gr.gr_context) + self.assertEqual(old_context[ID_VAR], 1) + self.assertEqual(new_context[ID_VAR], 2) + self.assertEqual(new_context.run(ID_VAR.get), 2) + gr.gr_context = old_context # assign non-None while suspended + gr.switch() + self.assertIs(gr.gr_context, new_context) + gr.gr_context = None # assign None while suspended + gr.switch() + self.assertIs(gr.gr_context, old_context) + gr.gr_context = None + gr.switch() + self.assertIsNone(gr.gr_context) + + # Make sure there are no reference leaks + gr = None + gc.collect() + # Python 3.14 elides reference counting operations + # in some cases. See https://github.com/python/cpython/pull/130708 + self.assertEqual(sys.getrefcount(old_context), 2 if not PY314 else 1) + self.assertEqual(sys.getrefcount(new_context), 2 if not PY314 else 1) + + def test_context_assignment_different_thread(self): + import threading + VAR_VAR.set(None) + ctx = Context() + + is_running = threading.Event() + should_suspend = threading.Event() + did_suspend = threading.Event() + should_exit = threading.Event() + holder = [] + + def greenlet_in_thread_fn(): + VAR_VAR.set(1) + is_running.set() + should_suspend.wait(10) + VAR_VAR.set(2) + getcurrent().parent.switch() + holder.append(VAR_VAR.get()) + + def thread_fn(): + gr = greenlet(greenlet_in_thread_fn) + gr.gr_context = ctx + holder.append(gr) + gr.switch() + did_suspend.set() + should_exit.wait(10) + gr.switch() + del gr + greenlet() # trigger cleanup + + thread = threading.Thread(target=thread_fn, daemon=True) + thread.start() + is_running.wait(10) + gr = holder[0] + + # Can't access or modify context if the greenlet is running + # in a different thread + with self.assertRaisesRegex(ValueError, "running in a different"): + getattr(gr, 'gr_context') + with self.assertRaisesRegex(ValueError, "running in a different"): + gr.gr_context = None + + should_suspend.set() + did_suspend.wait(10) + + # OK to access and modify context if greenlet is suspended + self.assertIs(gr.gr_context, ctx) + self.assertEqual(gr.gr_context[VAR_VAR], 2) + gr.gr_context = None + + should_exit.set() + thread.join(10) + + self.assertEqual(holder, [gr, None]) + + # Context can still be accessed/modified when greenlet is dead: + self.assertIsNone(gr.gr_context) + gr.gr_context = ctx + self.assertIs(gr.gr_context, ctx) + + # Otherwise we leak greenlets on some platforms. + # XXX: Should be able to do this automatically + del holder[:] + gr = None + thread = None + + def test_context_assignment_wrong_type(self): + g = greenlet() + with self.assertRaisesRegex(TypeError, + "greenlet context must be a contextvars.Context or None"): + g.gr_context = self + + +@skipIf(Context is not None, "ContextVar supported") +class NoContextVarsTests(TestCase): + def test_contextvars_errors(self): + let1 = greenlet(getcurrent().switch) + self.assertFalse(hasattr(let1, 'gr_context')) + with self.assertRaises(AttributeError): + getattr(let1, 'gr_context') + + with self.assertRaises(AttributeError): + let1.gr_context = None + + let1.switch() + + with self.assertRaises(AttributeError): + getattr(let1, 'gr_context') + + with self.assertRaises(AttributeError): + let1.gr_context = None + + del let1 + + +if __name__ == '__main__': + unittest.main() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_cpp.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_cpp.py new file mode 100644 index 0000000..2d0cc9c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_cpp.py @@ -0,0 +1,73 @@ +from __future__ import print_function +from __future__ import absolute_import + +import subprocess +import unittest + +import greenlet +from . import _test_extension_cpp +from . import TestCase +from . import WIN + +class CPPTests(TestCase): + def test_exception_switch(self): + greenlets = [] + for i in range(4): + g = greenlet.greenlet(_test_extension_cpp.test_exception_switch) + g.switch(i) + greenlets.append(g) + for i, g in enumerate(greenlets): + self.assertEqual(g.switch(), i) + + def _do_test_unhandled_exception(self, target): + import os + import sys + script = os.path.join( + os.path.dirname(__file__), + 'fail_cpp_exception.py', + ) + args = [sys.executable, script, target.__name__ if not isinstance(target, str) else target] + __traceback_info__ = args + with self.assertRaises(subprocess.CalledProcessError) as exc: + subprocess.check_output( + args, + encoding='utf-8', + stderr=subprocess.STDOUT + ) + + ex = exc.exception + expected_exit = self.get_expected_returncodes_for_aborted_process() + self.assertIn(ex.returncode, expected_exit) + self.assertIn('fail_cpp_exception is running', ex.output) + return ex.output + + + def test_unhandled_nonstd_exception_aborts(self): + # verify that plain unhandled throw aborts + self._do_test_unhandled_exception(_test_extension_cpp.test_exception_throw_nonstd) + + def test_unhandled_std_exception_aborts(self): + # verify that plain unhandled throw aborts + self._do_test_unhandled_exception(_test_extension_cpp.test_exception_throw_std) + + @unittest.skipIf(WIN, "XXX: This does not crash on Windows") + # Meaning the exception is getting lost somewhere... + def test_unhandled_std_exception_as_greenlet_function_aborts(self): + # verify that plain unhandled throw aborts + output = self._do_test_unhandled_exception('run_as_greenlet_target') + self.assertIn( + # We really expect this to be prefixed with "greenlet: Unhandled C++ exception:" + # as added by our handler for std::exception (see TUserGreenlet.cpp), but + # that's not correct everywhere --- our handler never runs before std::terminate + # gets called (for example, on arm32). + 'Thrown from an extension.', + output + ) + + def test_unhandled_exception_in_greenlet_aborts(self): + # verify that unhandled throw called in greenlet aborts too + self._do_test_unhandled_exception('run_unhandled_exception_in_greenlet_aborts') + + +if __name__ == '__main__': + unittest.main() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_extension_interface.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_extension_interface.py new file mode 100644 index 0000000..34b6656 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_extension_interface.py @@ -0,0 +1,115 @@ +from __future__ import print_function +from __future__ import absolute_import + +import sys + +import greenlet +from . import _test_extension +from . import TestCase + +# pylint:disable=c-extension-no-member + +class CAPITests(TestCase): + def test_switch(self): + self.assertEqual( + 50, _test_extension.test_switch(greenlet.greenlet(lambda: 50))) + + def test_switch_kwargs(self): + def adder(x, y): + return x * y + g = greenlet.greenlet(adder) + self.assertEqual(6, _test_extension.test_switch_kwargs(g, x=3, y=2)) + + def test_setparent(self): + # pylint:disable=disallowed-name + def foo(): + def bar(): + greenlet.getcurrent().parent.switch() + + # This final switch should go back to the main greenlet, since + # the test_setparent() function in the C extension should have + # reparented this greenlet. + greenlet.getcurrent().parent.switch() + raise AssertionError("Should never have reached this code") + child = greenlet.greenlet(bar) + child.switch() + greenlet.getcurrent().parent.switch(child) + greenlet.getcurrent().parent.throw( + AssertionError("Should never reach this code")) + foo_child = greenlet.greenlet(foo).switch() + self.assertEqual(None, _test_extension.test_setparent(foo_child)) + + def test_getcurrent(self): + _test_extension.test_getcurrent() + + def test_new_greenlet(self): + self.assertEqual(-15, _test_extension.test_new_greenlet(lambda: -15)) + + def test_raise_greenlet_dead(self): + self.assertRaises( + greenlet.GreenletExit, _test_extension.test_raise_dead_greenlet) + + def test_raise_greenlet_error(self): + self.assertRaises( + greenlet.error, _test_extension.test_raise_greenlet_error) + + def test_throw(self): + seen = [] + + def foo(): # pylint:disable=disallowed-name + try: + greenlet.getcurrent().parent.switch() + except ValueError: + seen.append(sys.exc_info()[1]) + except greenlet.GreenletExit: + raise AssertionError + g = greenlet.greenlet(foo) + g.switch() + _test_extension.test_throw(g) + self.assertEqual(len(seen), 1) + self.assertTrue( + isinstance(seen[0], ValueError), + "ValueError was not raised in foo()") + self.assertEqual( + str(seen[0]), + 'take that sucka!', + "message doesn't match") + + def test_non_traceback_param(self): + with self.assertRaises(TypeError) as exc: + _test_extension.test_throw_exact( + greenlet.getcurrent(), + Exception, + Exception(), + self + ) + self.assertEqual(str(exc.exception), + "throw() third argument must be a traceback object") + + def test_instance_of_wrong_type(self): + with self.assertRaises(TypeError) as exc: + _test_extension.test_throw_exact( + greenlet.getcurrent(), + Exception(), + BaseException(), + None, + ) + + self.assertEqual(str(exc.exception), + "instance exception may not have a separate value") + + def test_not_throwable(self): + with self.assertRaises(TypeError) as exc: + _test_extension.test_throw_exact( + greenlet.getcurrent(), + "abc", + None, + None, + ) + self.assertEqual(str(exc.exception), + "exceptions must be classes, or instances, not str") + + +if __name__ == '__main__': + import unittest + unittest.main() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_gc.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_gc.py new file mode 100644 index 0000000..994addb --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_gc.py @@ -0,0 +1,86 @@ +import gc + +import weakref + +import greenlet + + +from . import TestCase +from .leakcheck import fails_leakcheck +# These only work with greenlet gc support +# which is no longer optional. +assert greenlet.GREENLET_USE_GC + +class GCTests(TestCase): + def test_dead_circular_ref(self): + o = weakref.ref(greenlet.greenlet(greenlet.getcurrent).switch()) + gc.collect() + if o() is not None: + import sys + print("O IS NOT NONE.", sys.getrefcount(o())) + self.assertIsNone(o()) + self.assertFalse(gc.garbage, gc.garbage) + + def test_circular_greenlet(self): + class circular_greenlet(greenlet.greenlet): + self = None + o = circular_greenlet() + o.self = o + o = weakref.ref(o) + gc.collect() + self.assertIsNone(o()) + self.assertFalse(gc.garbage, gc.garbage) + + def test_inactive_ref(self): + class inactive_greenlet(greenlet.greenlet): + def __init__(self): + greenlet.greenlet.__init__(self, run=self.run) + + def run(self): + pass + o = inactive_greenlet() + o = weakref.ref(o) + gc.collect() + self.assertIsNone(o()) + self.assertFalse(gc.garbage, gc.garbage) + + @fails_leakcheck + def test_finalizer_crash(self): + # This test is designed to crash when active greenlets + # are made garbage collectable, until the underlying + # problem is resolved. How does it work: + # - order of object creation is important + # - array is created first, so it is moved to unreachable first + # - we create a cycle between a greenlet and this array + # - we create an object that participates in gc, is only + # referenced by a greenlet, and would corrupt gc lists + # on destruction, the easiest is to use an object with + # a finalizer + # - because array is the first object in unreachable it is + # cleared first, which causes all references to greenlet + # to disappear and causes greenlet to be destroyed, but since + # it is still live it causes a switch during gc, which causes + # an object with finalizer to be destroyed, which causes stack + # corruption and then a crash + + class object_with_finalizer(object): + def __del__(self): + pass + array = [] + parent = greenlet.getcurrent() + def greenlet_body(): + greenlet.getcurrent().object = object_with_finalizer() + try: + parent.switch() + except greenlet.GreenletExit: + print("Got greenlet exit!") + finally: + del greenlet.getcurrent().object + g = greenlet.greenlet(greenlet_body) + g.array = array + array.append(g) + g.switch() + del array + del g + greenlet.getcurrent() + gc.collect() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_generator.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_generator.py new file mode 100644 index 0000000..ca4a644 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_generator.py @@ -0,0 +1,59 @@ + +from greenlet import greenlet + +from . import TestCase + +class genlet(greenlet): + parent = None + def __init__(self, *args, **kwds): + self.args = args + self.kwds = kwds + + def run(self): + fn, = self.fn + fn(*self.args, **self.kwds) + + def __iter__(self): + return self + + def __next__(self): + self.parent = greenlet.getcurrent() + result = self.switch() + if self: + return result + + raise StopIteration + + next = __next__ + + +def Yield(value): + g = greenlet.getcurrent() + while not isinstance(g, genlet): + if g is None: + raise RuntimeError('yield outside a genlet') + g = g.parent + g.parent.switch(value) + + +def generator(func): + class Generator(genlet): + fn = (func,) + return Generator + +# ____________________________________________________________ + + +class GeneratorTests(TestCase): + def test_generator(self): + seen = [] + + def g(n): + for i in range(n): + seen.append(i) + Yield(i) + g = generator(g) + for _ in range(3): + for j in g(5): + seen.append(j) + self.assertEqual(seen, 3 * [0, 0, 1, 1, 2, 2, 3, 3, 4, 4]) diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_generator_nested.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_generator_nested.py new file mode 100644 index 0000000..8d752a6 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_generator_nested.py @@ -0,0 +1,168 @@ + +from greenlet import greenlet +from . import TestCase +from .leakcheck import fails_leakcheck + +class genlet(greenlet): + parent = None + def __init__(self, *args, **kwds): + self.args = args + self.kwds = kwds + self.child = None + + def run(self): + # Note the function is packed in a tuple + # to avoid creating a bound method for it. + fn, = self.fn + fn(*self.args, **self.kwds) + + def __iter__(self): + return self + + def set_child(self, child): + self.child = child + + def __next__(self): + if self.child: + child = self.child + while child.child: + tmp = child + child = child.child + tmp.child = None + + result = child.switch() + else: + self.parent = greenlet.getcurrent() + result = self.switch() + + if self: + return result + + raise StopIteration + + next = __next__ + +def Yield(value, level=1): + g = greenlet.getcurrent() + + while level != 0: + if not isinstance(g, genlet): + raise RuntimeError('yield outside a genlet') + if level > 1: + g.parent.set_child(g) + g = g.parent + level -= 1 + + g.switch(value) + + +def Genlet(func): + class TheGenlet(genlet): + fn = (func,) + return TheGenlet + +# ____________________________________________________________ + + +def g1(n, seen): + for i in range(n): + seen.append(i + 1) + yield i + + +def g2(n, seen): + for i in range(n): + seen.append(i + 1) + Yield(i) + +g2 = Genlet(g2) + + +def nested(i): + Yield(i) + + +def g3(n, seen): + for i in range(n): + seen.append(i + 1) + nested(i) +g3 = Genlet(g3) + + +def a(n): + if n == 0: + return + for ii in ax(n - 1): + Yield(ii) + Yield(n) +ax = Genlet(a) + + +def perms(l): + if len(l) > 1: + for e in l: + # No syntactical sugar for generator expressions + x = [Yield([e] + p) for p in perms([x for x in l if x != e])] + assert x + else: + Yield(l) +perms = Genlet(perms) + + +def gr1(n): + for ii in range(1, n): + Yield(ii) + Yield(ii * ii, 2) + +gr1 = Genlet(gr1) + + +def gr2(n, seen): + for ii in gr1(n): + seen.append(ii) + +gr2 = Genlet(gr2) + + +class NestedGeneratorTests(TestCase): + def test_layered_genlets(self): + seen = [] + for ii in gr2(5, seen): + seen.append(ii) + self.assertEqual(seen, [1, 1, 2, 4, 3, 9, 4, 16]) + + @fails_leakcheck + def test_permutations(self): + gen_perms = perms(list(range(4))) + permutations = list(gen_perms) + self.assertEqual(len(permutations), 4 * 3 * 2 * 1) + self.assertIn([0, 1, 2, 3], permutations) + self.assertIn([3, 2, 1, 0], permutations) + res = [] + for ii in zip(perms(list(range(4))), perms(list(range(3)))): + res.append(ii) + self.assertEqual( + res, + [([0, 1, 2, 3], [0, 1, 2]), ([0, 1, 3, 2], [0, 2, 1]), + ([0, 2, 1, 3], [1, 0, 2]), ([0, 2, 3, 1], [1, 2, 0]), + ([0, 3, 1, 2], [2, 0, 1]), ([0, 3, 2, 1], [2, 1, 0])]) + # XXX Test to make sure we are working as a generator expression + + def test_genlet_simple(self): + for g in g1, g2, g3: + seen = [] + for _ in range(3): + for j in g(5, seen): + seen.append(j) + self.assertEqual(seen, 3 * [1, 0, 2, 1, 3, 2, 4, 3, 5, 4]) + + def test_genlet_bad(self): + try: + Yield(10) + except RuntimeError: + pass + + def test_nested_genlets(self): + seen = [] + for ii in ax(5): + seen.append(ii) diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_greenlet.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_greenlet.py new file mode 100644 index 0000000..1fa4bd1 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_greenlet.py @@ -0,0 +1,1353 @@ +import gc +import sys +import time +import threading +import unittest + +from abc import ABCMeta +from abc import abstractmethod + +import greenlet +from greenlet import greenlet as RawGreenlet +from . import TestCase +from . import RUNNING_ON_MANYLINUX +from . import PY313 +from . import PY314 +from . import RUNNING_ON_FREETHREAD_BUILD +from .leakcheck import fails_leakcheck + + +# We manually manage locks in many tests +# pylint:disable=consider-using-with +# pylint:disable=too-many-public-methods +# This module is quite large. +# TODO: Refactor into separate test files. For example, +# put all the regression tests that used to produce +# crashes in test_greenlet_no_crash; put tests that DO deliberately crash +# the interpreter into test_greenlet_crash. +# pylint:disable=too-many-lines + +class SomeError(Exception): + pass + + +def fmain(seen): + try: + greenlet.getcurrent().parent.switch() + except: + seen.append(sys.exc_info()[0]) + raise + raise SomeError + + +def send_exception(g, exc): + # note: send_exception(g, exc) can be now done with g.throw(exc). + # the purpose of this test is to explicitly check the propagation rules. + def crasher(exc): + raise exc + g1 = RawGreenlet(crasher, parent=g) + g1.switch(exc) + + +class TestGreenlet(TestCase): + + def _do_simple_test(self): + lst = [] + + def f(): + lst.append(1) + greenlet.getcurrent().parent.switch() + lst.append(3) + g = RawGreenlet(f) + lst.append(0) + g.switch() + lst.append(2) + g.switch() + lst.append(4) + self.assertEqual(lst, list(range(5))) + + def test_simple(self): + self._do_simple_test() + + def test_switch_no_run_raises_AttributeError(self): + g = RawGreenlet() + with self.assertRaises(AttributeError) as exc: + g.switch() + + self.assertIn("run", str(exc.exception)) + + def test_throw_no_run_raises_AttributeError(self): + g = RawGreenlet() + with self.assertRaises(AttributeError) as exc: + g.throw(SomeError) + + self.assertIn("run", str(exc.exception)) + + def test_parent_equals_None(self): + g = RawGreenlet(parent=None) + self.assertIsNotNone(g) + self.assertIs(g.parent, greenlet.getcurrent()) + + def test_run_equals_None(self): + g = RawGreenlet(run=None) + self.assertIsNotNone(g) + self.assertIsNone(g.run) + + def test_two_children(self): + lst = [] + + def f(): + lst.append(1) + greenlet.getcurrent().parent.switch() + lst.extend([1, 1]) + g = RawGreenlet(f) + h = RawGreenlet(f) + g.switch() + self.assertEqual(len(lst), 1) + h.switch() + self.assertEqual(len(lst), 2) + h.switch() + self.assertEqual(len(lst), 4) + self.assertEqual(h.dead, True) + g.switch() + self.assertEqual(len(lst), 6) + self.assertEqual(g.dead, True) + + def test_two_recursive_children(self): + lst = [] + + def f(): + lst.append('b') + greenlet.getcurrent().parent.switch() + + def g(): + lst.append('a') + g = RawGreenlet(f) + g.switch() + lst.append('c') + self.assertEqual(sys.getrefcount(g), 2 if not PY314 else 1) + g = RawGreenlet(g) + # Python 3.14 elides reference counting operations + # in some cases. See https://github.com/python/cpython/pull/130708 + self.assertEqual(sys.getrefcount(g), 2 if not PY314 else 1) + g.switch() + self.assertEqual(lst, ['a', 'b', 'c']) + # Just the one in this frame, plus the one on the stack we pass to the function + self.assertEqual(sys.getrefcount(g), 2 if not PY314 else 1) + + def test_threads(self): + success = [] + + def f(): + self._do_simple_test() + success.append(True) + ths = [threading.Thread(target=f) for i in range(10)] + for th in ths: + th.start() + for th in ths: + th.join(10) + self.assertEqual(len(success), len(ths)) + + def test_exception(self): + seen = [] + g1 = RawGreenlet(fmain) + g2 = RawGreenlet(fmain) + g1.switch(seen) + g2.switch(seen) + g2.parent = g1 + + self.assertEqual(seen, []) + #with self.assertRaises(SomeError): + # p("***Switching back") + # g2.switch() + # Creating this as a bound method can reveal bugs that + # are hidden on newer versions of Python that avoid creating + # bound methods for direct expressions; IOW, don't use the `with` + # form! + self.assertRaises(SomeError, g2.switch) + self.assertEqual(seen, [SomeError]) + + value = g2.switch() + self.assertEqual(value, ()) + self.assertEqual(seen, [SomeError]) + + value = g2.switch(25) + self.assertEqual(value, 25) + self.assertEqual(seen, [SomeError]) + + + def test_send_exception(self): + seen = [] + g1 = RawGreenlet(fmain) + g1.switch(seen) + self.assertRaises(KeyError, send_exception, g1, KeyError) + self.assertEqual(seen, [KeyError]) + + def test_dealloc(self): + seen = [] + g1 = RawGreenlet(fmain) + g2 = RawGreenlet(fmain) + g1.switch(seen) + g2.switch(seen) + self.assertEqual(seen, []) + del g1 + gc.collect() + self.assertEqual(seen, [greenlet.GreenletExit]) + del g2 + gc.collect() + self.assertEqual(seen, [greenlet.GreenletExit, greenlet.GreenletExit]) + + def test_dealloc_catches_GreenletExit_throws_other(self): + def run(): + try: + greenlet.getcurrent().parent.switch() + except greenlet.GreenletExit: + raise SomeError from None + + g = RawGreenlet(run) + g.switch() + # Destroying the only reference to the greenlet causes it + # to get GreenletExit; when it in turn raises, even though we're the parent + # we don't get the exception, it just gets printed. + # When we run on 3.8 only, we can use sys.unraisablehook + oldstderr = sys.stderr + from io import StringIO + stderr = sys.stderr = StringIO() + try: + del g + finally: + sys.stderr = oldstderr + + v = stderr.getvalue() + self.assertIn("Exception", v) + self.assertIn('ignored', v) + self.assertIn("SomeError", v) + + + @unittest.skipIf( + PY313 and RUNNING_ON_MANYLINUX, + "Sometimes flaky (getting one GreenletExit in the second list)" + # Probably due to funky timing interactions? + # TODO: FIXME Make that work. + ) + + def test_dealloc_other_thread(self): + seen = [] + someref = [] + + bg_glet_created_running_and_no_longer_ref_in_bg = threading.Event() + fg_ref_released = threading.Event() + bg_should_be_clear = threading.Event() + ok_to_exit_bg_thread = threading.Event() + + def f(): + g1 = RawGreenlet(fmain) + g1.switch(seen) + someref.append(g1) + del g1 + gc.collect() + bg_glet_created_running_and_no_longer_ref_in_bg.set() + fg_ref_released.wait(3) + + RawGreenlet() # trigger release + bg_should_be_clear.set() + ok_to_exit_bg_thread.wait(3) + RawGreenlet() # One more time + + t = threading.Thread(target=f) + t.start() + bg_glet_created_running_and_no_longer_ref_in_bg.wait(10) + + self.assertEqual(seen, []) + self.assertEqual(len(someref), 1) + del someref[:] + if not RUNNING_ON_FREETHREAD_BUILD: + # The free-threaded GC is very different. In 3.14rc1, + # the free-threaded GC traverses ``g1``, realizes it is + # not referenced from anywhere else IT cares about, + # calls ``tp_clear`` and then ``green_dealloc``. This causes + # the greenlet to lose its reference to the main greenlet and thread + # in which it was running, which means we can no longer throw an + # exception into it, preventing the rest of this test from working. + # Standard 3.14 traverses the object but doesn't ``tp_clear`` or + # ``green_dealloc`` it. + gc.collect() + # g1 is not released immediately because it's from another thread; + # switching back to that thread will allocate a greenlet and thus + # trigger deletion actions. + self.assertEqual(seen, []) + fg_ref_released.set() + bg_should_be_clear.wait(3) + try: + self.assertEqual(seen, [greenlet.GreenletExit]) + finally: + ok_to_exit_bg_thread.set() + t.join(10) + del seen[:] + del someref[:] + + def test_frame(self): + def f1(): + f = sys._getframe(0) # pylint:disable=protected-access + self.assertEqual(f.f_back, None) + greenlet.getcurrent().parent.switch(f) + return "meaning of life" + g = RawGreenlet(f1) + frame = g.switch() + self.assertTrue(frame is g.gr_frame) + self.assertTrue(g) + + from_g = g.switch() + self.assertFalse(g) + self.assertEqual(from_g, 'meaning of life') + self.assertEqual(g.gr_frame, None) + + def test_thread_bug(self): + def runner(x): + g = RawGreenlet(lambda: time.sleep(x)) + g.switch() + t1 = threading.Thread(target=runner, args=(0.2,)) + t2 = threading.Thread(target=runner, args=(0.3,)) + t1.start() + t2.start() + t1.join(10) + t2.join(10) + + def test_switch_kwargs(self): + def run(a, b): + self.assertEqual(a, 4) + self.assertEqual(b, 2) + return 42 + x = RawGreenlet(run).switch(a=4, b=2) + self.assertEqual(x, 42) + + def test_switch_kwargs_to_parent(self): + def run(x): + greenlet.getcurrent().parent.switch(x=x) + greenlet.getcurrent().parent.switch(2, x=3) + return x, x ** 2 + g = RawGreenlet(run) + self.assertEqual({'x': 3}, g.switch(3)) + self.assertEqual(((2,), {'x': 3}), g.switch()) + self.assertEqual((3, 9), g.switch()) + + def test_switch_to_another_thread(self): + data = {} + created_event = threading.Event() + done_event = threading.Event() + + def run(): + data['g'] = RawGreenlet(lambda: None) + created_event.set() + done_event.wait(10) + thread = threading.Thread(target=run) + thread.start() + created_event.wait(10) + with self.assertRaises(greenlet.error): + data['g'].switch() + done_event.set() + thread.join(10) + # XXX: Should handle this automatically + data.clear() + + def test_exc_state(self): + def f(): + try: + raise ValueError('fun') + except: # pylint:disable=bare-except + exc_info = sys.exc_info() + RawGreenlet(h).switch() + self.assertEqual(exc_info, sys.exc_info()) + + def h(): + self.assertEqual(sys.exc_info(), (None, None, None)) + + RawGreenlet(f).switch() + + def test_instance_dict(self): + def f(): + greenlet.getcurrent().test = 42 + def deldict(g): + del g.__dict__ + def setdict(g, value): + g.__dict__ = value + g = RawGreenlet(f) + self.assertEqual(g.__dict__, {}) + g.switch() + self.assertEqual(g.test, 42) + self.assertEqual(g.__dict__, {'test': 42}) + g.__dict__ = g.__dict__ + self.assertEqual(g.__dict__, {'test': 42}) + self.assertRaises(TypeError, deldict, g) + self.assertRaises(TypeError, setdict, g, 42) + + def test_running_greenlet_has_no_run(self): + has_run = [] + def func(): + has_run.append( + hasattr(greenlet.getcurrent(), 'run') + ) + + g = RawGreenlet(func) + g.switch() + self.assertEqual(has_run, [False]) + + def test_deepcopy(self): + import copy + self.assertRaises(TypeError, copy.copy, RawGreenlet()) + self.assertRaises(TypeError, copy.deepcopy, RawGreenlet()) + + def test_parent_restored_on_kill(self): + hub = RawGreenlet(lambda: None) + main = greenlet.getcurrent() + result = [] + def worker(): + try: + # Wait to be killed by going back to the test. + main.switch() + except greenlet.GreenletExit: + # Resurrect and switch to parent + result.append(greenlet.getcurrent().parent) + result.append(greenlet.getcurrent()) + hub.switch() + g = RawGreenlet(worker, parent=hub) + g.switch() + # delete the only reference, thereby raising GreenletExit + del g + self.assertTrue(result) + self.assertIs(result[0], main) + self.assertIs(result[1].parent, hub) + # Delete them, thereby breaking the cycle between the greenlet + # and the frame, which otherwise would never be collectable + # XXX: We should be able to automatically fix this. + del result[:] + hub = None + main = None + + def test_parent_return_failure(self): + # No run causes AttributeError on switch + g1 = RawGreenlet() + # Greenlet that implicitly switches to parent + g2 = RawGreenlet(lambda: None, parent=g1) + # AttributeError should propagate to us, no fatal errors + with self.assertRaises(AttributeError): + g2.switch() + + def test_throw_exception_not_lost(self): + class mygreenlet(RawGreenlet): + def __getattribute__(self, name): + try: + raise Exception # pylint:disable=broad-exception-raised + except: # pylint:disable=bare-except + pass + return RawGreenlet.__getattribute__(self, name) + g = mygreenlet(lambda: None) + self.assertRaises(SomeError, g.throw, SomeError()) + + @fails_leakcheck + def _do_test_throw_to_dead_thread_doesnt_crash(self, wait_for_cleanup=False): + result = [] + def worker(): + greenlet.getcurrent().parent.switch() + + def creator(): + g = RawGreenlet(worker) + g.switch() + result.append(g) + if wait_for_cleanup: + # Let this greenlet eventually be cleaned up. + g.switch() + greenlet.getcurrent() + t = threading.Thread(target=creator) + t.start() + t.join(10) + del t + # But, depending on the operating system, the thread + # deallocator may not actually have run yet! So we can't be + # sure about the error message unless we wait. + if wait_for_cleanup: + self.wait_for_pending_cleanups() + with self.assertRaises(greenlet.error) as exc: + result[0].throw(SomeError) + + if not wait_for_cleanup: + s = str(exc.exception) + self.assertTrue( + s == "cannot switch to a different thread (which happens to have exited)" + or 'Cannot switch' in s + ) + else: + self.assertEqual( + str(exc.exception), + "cannot switch to a different thread (which happens to have exited)", + ) + + if hasattr(result[0].gr_frame, 'clear'): + # The frame is actually executing (it thinks), we can't clear it. + with self.assertRaises(RuntimeError): + result[0].gr_frame.clear() + # Unfortunately, this doesn't actually clear the references, they're in the + # fast local array. + if not wait_for_cleanup: + # f_locals has no clear method in Python 3.13 + if hasattr(result[0].gr_frame.f_locals, 'clear'): + result[0].gr_frame.f_locals.clear() + else: + self.assertIsNone(result[0].gr_frame) + + del creator + worker = None + del result[:] + # XXX: we ought to be able to automatically fix this. + # See issue 252 + self.expect_greenlet_leak = True # direct us not to wait for it to go away + + @fails_leakcheck + def test_throw_to_dead_thread_doesnt_crash(self): + self._do_test_throw_to_dead_thread_doesnt_crash() + + def test_throw_to_dead_thread_doesnt_crash_wait(self): + self._do_test_throw_to_dead_thread_doesnt_crash(True) + + @fails_leakcheck + def test_recursive_startup(self): + class convoluted(RawGreenlet): + def __init__(self): + RawGreenlet.__init__(self) + self.count = 0 + def __getattribute__(self, name): + if name == 'run' and self.count == 0: + self.count = 1 + self.switch(43) + return RawGreenlet.__getattribute__(self, name) + def run(self, value): + while True: + self.parent.switch(value) + g = convoluted() + self.assertEqual(g.switch(42), 43) + # Exits the running greenlet, otherwise it leaks + # XXX: We should be able to automatically fix this + #g.throw(greenlet.GreenletExit) + #del g + self.expect_greenlet_leak = True + + def test_threaded_updatecurrent(self): + # released when main thread should execute + lock1 = threading.Lock() + lock1.acquire() + # released when another thread should execute + lock2 = threading.Lock() + lock2.acquire() + class finalized(object): + def __del__(self): + # happens while in green_updatecurrent() in main greenlet + # should be very careful not to accidentally call it again + # at the same time we must make sure another thread executes + lock2.release() + lock1.acquire() + # now ts_current belongs to another thread + def deallocator(): + greenlet.getcurrent().parent.switch() + def fthread(): + lock2.acquire() + greenlet.getcurrent() + del g[0] + lock1.release() + lock2.acquire() + greenlet.getcurrent() + lock1.release() + main = greenlet.getcurrent() + g = [RawGreenlet(deallocator)] + g[0].bomb = finalized() + g[0].switch() + t = threading.Thread(target=fthread) + t.start() + # let another thread grab ts_current and deallocate g[0] + lock2.release() + lock1.acquire() + # this is the corner stone + # getcurrent() will notice that ts_current belongs to another thread + # and start the update process, which would notice that g[0] should + # be deallocated, and that will execute an object's finalizer. Now, + # that object will let another thread run so it can grab ts_current + # again, which would likely crash the interpreter if there's no + # check for this case at the end of green_updatecurrent(). This test + # passes if getcurrent() returns correct result, but it's likely + # to randomly crash if it's not anyway. + self.assertEqual(greenlet.getcurrent(), main) + # wait for another thread to complete, just in case + t.join(10) + + def test_dealloc_switch_args_not_lost(self): + seen = [] + def worker(): + # wait for the value + value = greenlet.getcurrent().parent.switch() + # delete all references to ourself + del worker[0] + initiator.parent = greenlet.getcurrent().parent + # switch to main with the value, but because + # ts_current is the last reference to us we + # return here immediately, where we resurrect ourself. + try: + greenlet.getcurrent().parent.switch(value) + finally: + seen.append(greenlet.getcurrent()) + def initiator(): + return 42 # implicitly falls thru to parent + + worker = [RawGreenlet(worker)] + + worker[0].switch() # prime worker + initiator = RawGreenlet(initiator, worker[0]) + value = initiator.switch() + self.assertTrue(seen) + self.assertEqual(value, 42) + + def test_tuple_subclass(self): + # The point of this test is to see what happens when a custom + # tuple subclass is used as an object passed directly to the C + # function ``green_switch``; part of ``green_switch`` checks + # the ``len()`` of the ``args`` tuple, and that can call back + # into Python. Here, when it calls back into Python, we + # recursively enter ``green_switch`` again. + + # This test is really only relevant on Python 2. The builtin + # `apply` function directly passes the given args tuple object + # to the underlying function, whereas the Python 3 version + # unpacks and repacks into an actual tuple. This could still + # happen using the C API on Python 3 though. We should write a + # builtin version of apply() ourself. + def _apply(func, a, k): + func(*a, **k) + + class mytuple(tuple): + def __len__(self): + greenlet.getcurrent().switch() + return tuple.__len__(self) + args = mytuple() + kwargs = dict(a=42) + def switchapply(): + _apply(greenlet.getcurrent().parent.switch, args, kwargs) + g = RawGreenlet(switchapply) + self.assertEqual(g.switch(), kwargs) + + def test_abstract_subclasses(self): + AbstractSubclass = ABCMeta( + 'AbstractSubclass', + (RawGreenlet,), + {'run': abstractmethod(lambda self: None)}) + + class BadSubclass(AbstractSubclass): + pass + + class GoodSubclass(AbstractSubclass): + def run(self): + pass + + GoodSubclass() # should not raise + self.assertRaises(TypeError, BadSubclass) + + def test_implicit_parent_with_threads(self): + if not gc.isenabled(): + return # cannot test with disabled gc + N = gc.get_threshold()[0] + if N < 50: + return # cannot test with such a small N + def attempt(): + lock1 = threading.Lock() + lock1.acquire() + lock2 = threading.Lock() + lock2.acquire() + recycled = [False] + def another_thread(): + lock1.acquire() # wait for gc + greenlet.getcurrent() # update ts_current + lock2.release() # release gc + t = threading.Thread(target=another_thread) + t.start() + class gc_callback(object): + def __del__(self): + lock1.release() + lock2.acquire() + recycled[0] = True + class garbage(object): + def __init__(self): + self.cycle = self + self.callback = gc_callback() + l = [] + x = range(N*2) + current = greenlet.getcurrent() + g = garbage() + for _ in x: + g = None # lose reference to garbage + if recycled[0]: + # gc callback called prematurely + t.join(10) + return False + last = RawGreenlet() + if recycled[0]: + break # yes! gc called in green_new + l.append(last) # increase allocation counter + else: + # gc callback not called when expected + gc.collect() + if recycled[0]: + t.join(10) + return False + self.assertEqual(last.parent, current) + for g in l: + self.assertEqual(g.parent, current) + return True + for _ in range(5): + if attempt(): + break + + def test_issue_245_reference_counting_subclass_no_threads(self): + # https://github.com/python-greenlet/greenlet/issues/245 + # Before the fix, this crashed pretty reliably on + # Python 3.10, at least on macOS; but much less reliably on other + # interpreters (memory layout must have changed). + # The threaded test crashed more reliably on more interpreters. + from greenlet import getcurrent + from greenlet import GreenletExit + + class Greenlet(RawGreenlet): + pass + + initial_refs = sys.getrefcount(Greenlet) + # This has to be an instance variable because + # Python 2 raises a SyntaxError if we delete a local + # variable referenced in an inner scope. + self.glets = [] # pylint:disable=attribute-defined-outside-init + + def greenlet_main(): + try: + getcurrent().parent.switch() + except GreenletExit: + self.glets.append(getcurrent()) + + # Before the + for _ in range(10): + Greenlet(greenlet_main).switch() + + del self.glets + if RUNNING_ON_FREETHREAD_BUILD: + # Free-threaded builds make types immortal, which gives us + # weird numbers here, and we actually do APPEAR to end + # up with one more reference than we started with, at least on 3.14. + # If we change the code in green_dealloc to avoid increffing the type + # (which fixed this initial bug), then our leakchecks find other objects + # that have leaked, including a tuple, a dict, and a type. So that's not the + # right solution. Instead we change the test: + # XXX: FIXME: Is there a better way? + self.assertGreaterEqual(sys.getrefcount(Greenlet), initial_refs) + else: + self.assertEqual(sys.getrefcount(Greenlet), initial_refs) + + @unittest.skipIf( + PY313 and RUNNING_ON_MANYLINUX, + "The manylinux images appear to hang on this test on 3.13rc2" + # Or perhaps I just got tired of waiting for the 450s timeout. + # Still, it shouldn't take anywhere near that long. Does not reproduce in + # Ubuntu images, on macOS or Windows. + ) + def test_issue_245_reference_counting_subclass_threads(self): + # https://github.com/python-greenlet/greenlet/issues/245 + from threading import Thread + from threading import Event + + from greenlet import getcurrent + + class MyGreenlet(RawGreenlet): + pass + + glets = [] + ref_cleared = Event() + + def greenlet_main(): + getcurrent().parent.switch() + + def thread_main(greenlet_running_event): + mine = MyGreenlet(greenlet_main) + glets.append(mine) + # The greenlets being deleted must be active + mine.switch() + # Don't keep any reference to it in this thread + del mine + # Let main know we published our greenlet. + greenlet_running_event.set() + # Wait for main to let us know the references are + # gone and the greenlet objects no longer reachable + ref_cleared.wait(10) + # The creating thread must call getcurrent() (or a few other + # greenlet APIs) because that's when the thread-local list of dead + # greenlets gets cleared. + getcurrent() + + # We start with 3 references to the subclass: + # - This module + # - Its __mro__ + # - The __subclassess__ attribute of greenlet + # - (If we call gc.get_referents(), we find four entries, including + # some other tuple ``(greenlet)`` that I'm not sure about but must be part + # of the machinery.) + # + # On Python 3.10 it's often enough to just run 3 threads; on Python 2.7, + # more threads are needed, and the results are still + # non-deterministic. Presumably the memory layouts are different + initial_refs = sys.getrefcount(MyGreenlet) + thread_ready_events = [] + thread_count = initial_refs + 45 + if RUNNING_ON_FREETHREAD_BUILD: + # types are immortal, so this is a HUGE number most likely, + # and we can't create that many threads. + thread_count = 50 + for _ in range(thread_count): + event = Event() + thread = Thread(target=thread_main, args=(event,)) + thread_ready_events.append(event) + thread.start() + + + for done_event in thread_ready_events: + done_event.wait(10) + + + del glets[:] + ref_cleared.set() + # Let any other thread run; it will crash the interpreter + # if not fixed (or silently corrupt memory and we possibly crash + # later). + self.wait_for_pending_cleanups() + self.assertEqual(sys.getrefcount(MyGreenlet), initial_refs) + + def test_falling_off_end_switches_to_unstarted_parent_raises_error(self): + def no_args(): + return 13 + + parent_never_started = RawGreenlet(no_args) + + def leaf(): + return 42 + + child = RawGreenlet(leaf, parent_never_started) + + # Because the run function takes to arguments + with self.assertRaises(TypeError): + child.switch() + + def test_falling_off_end_switches_to_unstarted_parent_works(self): + def one_arg(x): + return (x, 24) + + parent_never_started = RawGreenlet(one_arg) + + def leaf(): + return 42 + + child = RawGreenlet(leaf, parent_never_started) + + result = child.switch() + self.assertEqual(result, (42, 24)) + + def test_switch_to_dead_greenlet_with_unstarted_perverse_parent(self): + class Parent(RawGreenlet): + def __getattribute__(self, name): + if name == 'run': + raise SomeError + + + parent_never_started = Parent() + seen = [] + child = RawGreenlet(lambda: seen.append(42), parent_never_started) + # Because we automatically start the parent when the child is + # finished + with self.assertRaises(SomeError): + child.switch() + + self.assertEqual(seen, [42]) + + with self.assertRaises(SomeError): + child.switch() + self.assertEqual(seen, [42]) + + def test_switch_to_dead_greenlet_reparent(self): + seen = [] + parent_never_started = RawGreenlet(lambda: seen.append(24)) + child = RawGreenlet(lambda: seen.append(42)) + + child.switch() + self.assertEqual(seen, [42]) + + child.parent = parent_never_started + # This actually is the same as switching to the parent. + result = child.switch() + self.assertIsNone(result) + self.assertEqual(seen, [42, 24]) + + def test_can_access_f_back_of_suspended_greenlet(self): + # This tests our frame rewriting to work around Python 3.12+ having + # some interpreter frames on the C stack. It will crash in the absence + # of that logic. + main = greenlet.getcurrent() + + def outer(): + inner() + + def inner(): + main.switch(sys._getframe(0)) + + hub = RawGreenlet(outer) + # start it + hub.switch() + + # start another greenlet to make sure we aren't relying on + # anything in `hub` still being on the C stack + unrelated = RawGreenlet(lambda: None) + unrelated.switch() + + # now it is suspended + self.assertIsNotNone(hub.gr_frame) + self.assertEqual(hub.gr_frame.f_code.co_name, "inner") + self.assertIsNotNone(hub.gr_frame.f_back) + self.assertEqual(hub.gr_frame.f_back.f_code.co_name, "outer") + # The next line is what would crash + self.assertIsNone(hub.gr_frame.f_back.f_back) + + def test_get_stack_with_nested_c_calls(self): + from functools import partial + from . import _test_extension_cpp + + def recurse(v): + if v > 0: + return v * _test_extension_cpp.test_call(partial(recurse, v - 1)) + return greenlet.getcurrent().parent.switch() + + gr = RawGreenlet(recurse) + gr.switch(5) + frame = gr.gr_frame + for i in range(5): + self.assertEqual(frame.f_locals["v"], i) + frame = frame.f_back + self.assertEqual(frame.f_locals["v"], 5) + self.assertIsNone(frame.f_back) + self.assertEqual(gr.switch(10), 1200) # 1200 = 5! * 10 + + def test_frames_always_exposed(self): + # On Python 3.12 this will crash if we don't set the + # gr_frames_always_exposed attribute. More background: + # https://github.com/python-greenlet/greenlet/issues/388 + main = greenlet.getcurrent() + + def outer(): + inner(sys._getframe(0)) + + def inner(frame): + main.switch(frame) + + gr = RawGreenlet(outer) + frame = gr.switch() + + # Do something else to clobber the part of the C stack used by `gr`, + # so we can't skate by on "it just happened to still be there" + unrelated = RawGreenlet(lambda: None) + unrelated.switch() + + self.assertEqual(frame.f_code.co_name, "outer") + # The next line crashes on 3.12 if we haven't exposed the frames. + self.assertIsNone(frame.f_back) + + +class TestGreenletSetParentErrors(TestCase): + def test_threaded_reparent(self): + data = {} + created_event = threading.Event() + done_event = threading.Event() + + def run(): + data['g'] = RawGreenlet(lambda: None) + created_event.set() + done_event.wait(10) + + def blank(): + greenlet.getcurrent().parent.switch() + + thread = threading.Thread(target=run) + thread.start() + created_event.wait(10) + g = RawGreenlet(blank) + g.switch() + with self.assertRaises(ValueError) as exc: + g.parent = data['g'] + done_event.set() + thread.join(10) + + self.assertEqual(str(exc.exception), "parent cannot be on a different thread") + + def test_unexpected_reparenting(self): + another = [] + def worker(): + g = RawGreenlet(lambda: None) + another.append(g) + g.switch() + t = threading.Thread(target=worker) + t.start() + t.join(10) + # The first time we switch (running g_initialstub(), which is + # when we look up the run attribute) we attempt to change the + # parent to one from another thread (which also happens to be + # dead). ``g_initialstub()`` should detect this and raise a + # greenlet error. + # + # EXCEPT: With the fix for #252, this is actually detected + # sooner, when setting the parent itself. Prior to that fix, + # the main greenlet from the background thread kept a valid + # value for ``run_info``, and appeared to be a valid parent + # until we actually started the greenlet. But now that it's + # cleared, this test is catching whether ``green_setparent`` + # can detect the dead thread. + # + # Further refactoring once again changes this back to a greenlet.error + # + # We need to wait for the cleanup to happen, but we're + # deliberately leaking a main greenlet here. + self.wait_for_pending_cleanups(initial_main_greenlets=self.main_greenlets_before_test + 1) + + class convoluted(RawGreenlet): + def __getattribute__(self, name): + if name == 'run': + self.parent = another[0] # pylint:disable=attribute-defined-outside-init + return RawGreenlet.__getattribute__(self, name) + g = convoluted(lambda: None) + with self.assertRaises(greenlet.error) as exc: + g.switch() + self.assertEqual(str(exc.exception), + "cannot switch to a different thread (which happens to have exited)") + del another[:] + + def test_unexpected_reparenting_thread_running(self): + # Like ``test_unexpected_reparenting``, except the background thread is + # actually still alive. + another = [] + switched_to_greenlet = threading.Event() + keep_main_alive = threading.Event() + def worker(): + g = RawGreenlet(lambda: None) + another.append(g) + g.switch() + switched_to_greenlet.set() + keep_main_alive.wait(10) + class convoluted(RawGreenlet): + def __getattribute__(self, name): + if name == 'run': + self.parent = another[0] # pylint:disable=attribute-defined-outside-init + return RawGreenlet.__getattribute__(self, name) + + t = threading.Thread(target=worker) + t.start() + + switched_to_greenlet.wait(10) + try: + g = convoluted(lambda: None) + + with self.assertRaises(greenlet.error) as exc: + g.switch() + self.assertIn("Cannot switch to a different thread", str(exc.exception)) + self.assertIn("Expected", str(exc.exception)) + self.assertIn("Current", str(exc.exception)) + finally: + keep_main_alive.set() + t.join(10) + # XXX: Should handle this automatically. + del another[:] + + def test_cannot_delete_parent(self): + worker = RawGreenlet(lambda: None) + self.assertIs(worker.parent, greenlet.getcurrent()) + + with self.assertRaises(AttributeError) as exc: + del worker.parent + self.assertEqual(str(exc.exception), "can't delete attribute") + + def test_cannot_delete_parent_of_main(self): + with self.assertRaises(AttributeError) as exc: + del greenlet.getcurrent().parent + self.assertEqual(str(exc.exception), "can't delete attribute") + + + def test_main_greenlet_parent_is_none(self): + # assuming we're in a main greenlet here. + self.assertIsNone(greenlet.getcurrent().parent) + + def test_set_parent_wrong_types(self): + def bg(): + # Go back to main. + greenlet.getcurrent().parent.switch() + + def check(glet): + for p in None, 1, self, "42": + with self.assertRaises(TypeError) as exc: + glet.parent = p + + self.assertEqual( + str(exc.exception), + "GreenletChecker: Expected any type of greenlet, not " + type(p).__name__) + + # First, not running + g = RawGreenlet(bg) + self.assertFalse(g) + check(g) + + # Then when running. + g.switch() + self.assertTrue(g) + check(g) + + # Let it finish + g.switch() + + + def test_trivial_cycle(self): + glet = RawGreenlet(lambda: None) + with self.assertRaises(ValueError) as exc: + glet.parent = glet + self.assertEqual(str(exc.exception), "cyclic parent chain") + + def test_trivial_cycle_main(self): + # This used to produce a ValueError, but we catch it earlier than that now. + with self.assertRaises(AttributeError) as exc: + greenlet.getcurrent().parent = greenlet.getcurrent() + self.assertEqual(str(exc.exception), "cannot set the parent of a main greenlet") + + def test_deeper_cycle(self): + g1 = RawGreenlet(lambda: None) + g2 = RawGreenlet(lambda: None) + g3 = RawGreenlet(lambda: None) + + g1.parent = g2 + g2.parent = g3 + with self.assertRaises(ValueError) as exc: + g3.parent = g1 + self.assertEqual(str(exc.exception), "cyclic parent chain") + + +class TestRepr(TestCase): + + def assertEndsWith(self, got, suffix): + self.assertTrue(got.endswith(suffix), (got, suffix)) + + def test_main_while_running(self): + r = repr(greenlet.getcurrent()) + self.assertEndsWith(r, " current active started main>") + + def test_main_in_background(self): + main = greenlet.getcurrent() + def run(): + return repr(main) + + g = RawGreenlet(run) + r = g.switch() + self.assertEndsWith(r, ' suspended active started main>') + + def test_initial(self): + r = repr(RawGreenlet()) + self.assertEndsWith(r, ' pending>') + + def test_main_from_other_thread(self): + main = greenlet.getcurrent() + + class T(threading.Thread): + original_main = thread_main = None + main_glet = None + def run(self): + self.original_main = repr(main) + self.main_glet = greenlet.getcurrent() + self.thread_main = repr(self.main_glet) + + t = T() + t.start() + t.join(10) + + self.assertEndsWith(t.original_main, ' suspended active started main>') + self.assertEndsWith(t.thread_main, ' current active started main>') + # give the machinery time to notice the death of the thread, + # and clean it up. Note that we don't use + # ``expect_greenlet_leak`` or wait_for_pending_cleanups, + # because at this point we know we have an extra greenlet + # still reachable. + for _ in range(3): + time.sleep(0.001) + + # In the past, main greenlets, even from dead threads, never + # really appear dead. We have fixed that, and we also report + # that the thread is dead in the repr. (Do this multiple times + # to make sure that we don't self-modify and forget our state + # in the C++ code). + for _ in range(3): + self.assertTrue(t.main_glet.dead) + r = repr(t.main_glet) + self.assertEndsWith(r, ' (thread exited) dead>') + + def test_dead(self): + g = RawGreenlet(lambda: None) + g.switch() + self.assertEndsWith(repr(g), ' dead>') + self.assertNotIn('suspended', repr(g)) + self.assertNotIn('started', repr(g)) + self.assertNotIn('active', repr(g)) + + def test_formatting_produces_native_str(self): + # https://github.com/python-greenlet/greenlet/issues/218 + # %s formatting on Python 2 was producing unicode, not str. + + g_dead = RawGreenlet(lambda: None) + g_not_started = RawGreenlet(lambda: None) + g_cur = greenlet.getcurrent() + + for g in g_dead, g_not_started, g_cur: + + self.assertIsInstance( + '%s' % (g,), + str + ) + self.assertIsInstance( + '%r' % (g,), + str, + ) + + +class TestMainGreenlet(TestCase): + # Tests some implementation details, and relies on some + # implementation details. + + def _check_current_is_main(self): + # implementation detail + assert 'main' in repr(greenlet.getcurrent()) + + t = type(greenlet.getcurrent()) + assert 'main' not in repr(t) + return t + + def test_main_greenlet_type_can_be_subclassed(self): + main_type = self._check_current_is_main() + subclass = type('subclass', (main_type,), {}) + self.assertIsNotNone(subclass) + + def test_main_greenlet_is_greenlet(self): + self._check_current_is_main() + self.assertIsInstance(greenlet.getcurrent(), RawGreenlet) + + + +class TestBrokenGreenlets(TestCase): + # Tests for things that used to, or still do, terminate the interpreter. + # This often means doing unsavory things. + + def test_failed_to_initialstub(self): + def func(): + raise AssertionError("Never get here") + + + g = greenlet._greenlet.UnswitchableGreenlet(func) + g.force_switch_error = True + + with self.assertRaisesRegex(SystemError, + "Failed to switch stacks into a greenlet for the first time."): + g.switch() + + def test_failed_to_switch_into_running(self): + runs = [] + def func(): + runs.append(1) + greenlet.getcurrent().parent.switch() + runs.append(2) + greenlet.getcurrent().parent.switch() + runs.append(3) # pragma: no cover + + g = greenlet._greenlet.UnswitchableGreenlet(func) + g.switch() + self.assertEqual(runs, [1]) + g.switch() + self.assertEqual(runs, [1, 2]) + g.force_switch_error = True + + with self.assertRaisesRegex(SystemError, + "Failed to switch stacks into a running greenlet."): + g.switch() + + # If we stopped here, we would fail the leakcheck, because we've left + # the ``inner_bootstrap()`` C frame and its descendents hanging around, + # which have a bunch of Python references. They'll never get cleaned up + # if we don't let the greenlet finish. + g.force_switch_error = False + g.switch() + self.assertEqual(runs, [1, 2, 3]) + + def test_failed_to_slp_switch_into_running(self): + ex = self.assertScriptRaises('fail_slp_switch.py') + + self.assertIn('fail_slp_switch is running', ex.output) + self.assertIn(ex.returncode, self.get_expected_returncodes_for_aborted_process()) + + def test_reentrant_switch_two_greenlets(self): + # Before we started capturing the arguments in g_switch_finish, this could crash. + output = self.run_script('fail_switch_two_greenlets.py') + self.assertIn('In g1_run', output) + self.assertIn('TRACE', output) + self.assertIn('LEAVE TRACE', output) + self.assertIn('Falling off end of main', output) + self.assertIn('Falling off end of g1_run', output) + self.assertIn('Falling off end of g2', output) + + def test_reentrant_switch_three_greenlets(self): + # On debug builds of greenlet, this used to crash with an assertion error; + # on non-debug versions, it ran fine (which it should not do!). + # Now it always crashes correctly with a TypeError + ex = self.assertScriptRaises('fail_switch_three_greenlets.py', exitcodes=(1,)) + + self.assertIn('TypeError', ex.output) + self.assertIn('positional arguments', ex.output) + + def test_reentrant_switch_three_greenlets2(self): + # This actually passed on debug and non-debug builds. It + # should probably have been triggering some debug assertions + # but it didn't. + # + # I think the fixes for the above test also kicked in here. + output = self.run_script('fail_switch_three_greenlets2.py') + self.assertIn( + "RESULTS: [('trace', 'switch'), " + "('trace', 'switch'), ('g2 arg', 'g2 from tracefunc'), " + "('trace', 'switch'), ('main g1', 'from g2_run'), ('trace', 'switch'), " + "('g1 arg', 'g1 from main'), ('trace', 'switch'), ('main g2', 'from g1_run'), " + "('trace', 'switch'), ('g1 from parent', 'g1 from main 2'), ('trace', 'switch'), " + "('main g1.2', 'g1 done'), ('trace', 'switch'), ('g2 from parent', ()), " + "('trace', 'switch'), ('main g2.2', 'g2 done')]", + output + ) + + def test_reentrant_switch_GreenletAlreadyStartedInPython(self): + output = self.run_script('fail_initialstub_already_started.py') + + self.assertIn( + "RESULTS: ['Begin C', 'Switch to b from B.__getattribute__ in C', " + "('Begin B', ()), '_B_run switching to main', ('main from c', 'From B'), " + "'B.__getattribute__ back from main in C', ('Begin A', (None,)), " + "('A dead?', True, 'B dead?', True, 'C dead?', False), " + "'C done', ('main from c.2', None)]", + output + ) + + def test_reentrant_switch_run_callable_has_del(self): + output = self.run_script('fail_clearing_run_switches.py') + self.assertIn( + "RESULTS [" + "('G.__getattribute__', 'run'), ('RunCallable', '__del__'), " + "('main: g.switch()', 'from RunCallable'), ('run_func', 'enter')" + "]", + output + ) + +if __name__ == '__main__': + unittest.main() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_greenlet_trash.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_greenlet_trash.py new file mode 100644 index 0000000..c1fc137 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_greenlet_trash.py @@ -0,0 +1,187 @@ +# -*- coding: utf-8 -*- +""" +Tests for greenlets interacting with the CPython trash can API. + +The CPython trash can API is not designed to be re-entered from a +single thread. But this can happen using greenlets, if something +during the object deallocation process switches greenlets, and this second +greenlet then causes the trash can to get entered again. Here, we do this +very explicitly, but in other cases (like gevent) it could be arbitrarily more +complicated: for example, a weakref callback might try to acquire a lock that's +already held by another greenlet; that would allow a greenlet switch to occur. + +See https://github.com/gevent/gevent/issues/1909 + +This test is fragile and relies on details of the CPython +implementation (like most of the rest of this package): + + - We enter the trashcan and deferred deallocation after + ``_PyTrash_UNWIND_LEVEL`` calls. This constant, defined in + CPython's object.c, is generally 50. That's basically how many objects are required to + get us into the deferred deallocation situation. + + - The test fails by hitting an ``assert()`` in object.c; if the + build didn't enable assert, then we don't catch this. + + - If the test fails in that way, the interpreter crashes. +""" +from __future__ import print_function, absolute_import, division + +import unittest + + +class TestTrashCanReEnter(unittest.TestCase): + + def test_it(self): + try: + # pylint:disable-next=no-name-in-module + from greenlet._greenlet import get_tstate_trash_delete_nesting # pylint:disable=unused-import + except ImportError: + import sys + # Python 3.13 has not "trash delete nesting" anymore (but "delete later") + assert sys.version_info[:2] >= (3, 13) + self.skipTest("get_tstate_trash_delete_nesting is not available.") + + # Try several times to trigger it, because it isn't 100% + # reliable. + for _ in range(10): + self.check_it() + + def check_it(self): # pylint:disable=too-many-statements + import greenlet + from greenlet._greenlet import get_tstate_trash_delete_nesting # pylint:disable=no-name-in-module + main = greenlet.getcurrent() + + assert get_tstate_trash_delete_nesting() == 0 + + # We expect to be in deferred deallocation after this many + # deallocations have occurred. TODO: I wish we had a better way to do + # this --- that was before get_tstate_trash_delete_nesting; perhaps + # we can use that API to do better? + TRASH_UNWIND_LEVEL = 50 + # How many objects to put in a container; it's the container that + # queues objects for deferred deallocation. + OBJECTS_PER_CONTAINER = 500 + + class Dealloc: # define the class here because we alter class variables each time we run. + """ + An object with a ``__del__`` method. When it starts getting deallocated + from a deferred trash can run, it switches greenlets, allocates more objects + which then also go in the trash can. If we don't save state appropriately, + nesting gets out of order and we can crash the interpreter. + """ + + #: Has our deallocation actually run and switched greenlets? + #: When it does, this will be set to the current greenlet. This should + #: be happening in the main greenlet, so we check that down below. + SPAWNED = False + + #: Has the background greenlet run? + BG_RAN = False + + BG_GLET = None + + #: How many of these things have ever been allocated. + CREATED = 0 + + #: How many of these things have ever been deallocated. + DESTROYED = 0 + + #: How many were destroyed not in the main greenlet. There should always + #: be some. + #: If the test is broken or things change in the trashcan implementation, + #: this may not be correct. + DESTROYED_BG = 0 + + def __init__(self, sequence_number): + """ + :param sequence_number: The ordinal of this object during + one particular creation run. This is used to detect (guess, really) + when we have entered the trash can's deferred deallocation. + """ + self.i = sequence_number + Dealloc.CREATED += 1 + + def __del__(self): + if self.i == TRASH_UNWIND_LEVEL and not self.SPAWNED: + Dealloc.SPAWNED = greenlet.getcurrent() + other = Dealloc.BG_GLET = greenlet.greenlet(background_greenlet) + x = other.switch() + assert x == 42 + # It's important that we don't switch back to the greenlet, + # we leave it hanging there in an incomplete state. But we don't let it + # get collected, either. If we complete it now, while we're still + # in the scope of the initial trash can, things work out and we + # don't see the problem. We need this greenlet to complete + # at some point in the future, after we've exited this trash can invocation. + del other + elif self.i == 40 and greenlet.getcurrent() is not main: + Dealloc.BG_RAN = True + try: + main.switch(42) + except greenlet.GreenletExit as ex: + # We expect this; all references to us go away + # while we're still running, and we need to finish deleting + # ourself. + Dealloc.BG_RAN = type(ex) + del ex + + # Record the fact that we're dead last of all. This ensures that + # we actually get returned too. + Dealloc.DESTROYED += 1 + if greenlet.getcurrent() is not main: + Dealloc.DESTROYED_BG += 1 + + + def background_greenlet(): + # We direct through a second function, instead of + # directly calling ``make_some()``, so that we have complete + # control over when these objects are destroyed: we need them + # to be destroyed in the context of the background greenlet + t = make_some() + del t # Triggere deletion. + + def make_some(): + t = () + i = OBJECTS_PER_CONTAINER + while i: + # Nest the tuples; it's the recursion that gets us + # into trash. + t = (Dealloc(i), t) + i -= 1 + return t + + + some = make_some() + self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER) + self.assertEqual(Dealloc.DESTROYED, 0) + + # If we're going to crash, it should be on the following line. + # We only crash if ``assert()`` is enabled, of course. + del some + + # For non-debug builds of CPython, we won't crash. The best we can do is check + # the nesting level explicitly. + self.assertEqual(0, get_tstate_trash_delete_nesting()) + + # Discard this, raising GreenletExit into where it is waiting. + Dealloc.BG_GLET = None + # The same nesting level maintains. + self.assertEqual(0, get_tstate_trash_delete_nesting()) + + # We definitely cleaned some up in the background + self.assertGreater(Dealloc.DESTROYED_BG, 0) + + # Make sure all the cleanups happened. + self.assertIs(Dealloc.SPAWNED, main) + self.assertTrue(Dealloc.BG_RAN) + self.assertEqual(Dealloc.BG_RAN, greenlet.GreenletExit) + self.assertEqual(Dealloc.CREATED, Dealloc.DESTROYED ) + self.assertEqual(Dealloc.CREATED, OBJECTS_PER_CONTAINER * 2) + + import gc + gc.collect() + + +if __name__ == '__main__': + unittest.main() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_leaks.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_leaks.py new file mode 100644 index 0000000..e09da7d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_leaks.py @@ -0,0 +1,457 @@ +# -*- coding: utf-8 -*- +""" +Testing scenarios that may have leaked. +""" +from __future__ import print_function, absolute_import, division + +import sys +import gc + +import time +import weakref +import threading + + +import greenlet +from . import TestCase +from . import PY314 +from . import RUNNING_ON_FREETHREAD_BUILD +from .leakcheck import fails_leakcheck +from .leakcheck import ignores_leakcheck +from .leakcheck import RUNNING_ON_MANYLINUX + + +# pylint:disable=protected-access + +assert greenlet.GREENLET_USE_GC # Option to disable this was removed in 1.0 + +class HasFinalizerTracksInstances(object): + EXTANT_INSTANCES = set() + def __init__(self, msg): + self.msg = sys.intern(msg) + self.EXTANT_INSTANCES.add(id(self)) + def __del__(self): + self.EXTANT_INSTANCES.remove(id(self)) + def __repr__(self): + return "" % ( + id(self), self.msg + ) + @classmethod + def reset(cls): + cls.EXTANT_INSTANCES.clear() + + +def fails_leakcheck_except_on_free_thraded(func): + if RUNNING_ON_FREETHREAD_BUILD: + # These all seem to pass on free threading because + # of the changes to the garbage collector + return func + return fails_leakcheck(func) + + +class TestLeaks(TestCase): + + def test_arg_refs(self): + args = ('a', 'b', 'c') + refcount_before = sys.getrefcount(args) + # pylint:disable=unnecessary-lambda + g = greenlet.greenlet( + lambda *args: greenlet.getcurrent().parent.switch(*args)) + for _ in range(100): + g.switch(*args) + self.assertEqual(sys.getrefcount(args), refcount_before) + + def test_kwarg_refs(self): + kwargs = {} + self.assertEqual(sys.getrefcount(kwargs), 2 if not PY314 else 1) + # pylint:disable=unnecessary-lambda + g = greenlet.greenlet( + lambda **gkwargs: greenlet.getcurrent().parent.switch(**gkwargs)) + for _ in range(100): + g.switch(**kwargs) + # Python 3.14 elides reference counting operations + # in some cases. See https://github.com/python/cpython/pull/130708 + self.assertEqual(sys.getrefcount(kwargs), 2 if not PY314 else 1) + + + @staticmethod + def __recycle_threads(): + # By introducing a thread that does sleep we allow other threads, + # that have triggered their __block condition, but did not have a + # chance to deallocate their thread state yet, to finally do so. + # The way it works is by requiring a GIL switch (different thread), + # which does a GIL release (sleep), which might do a GIL switch + # to finished threads and allow them to clean up. + def worker(): + time.sleep(0.001) + t = threading.Thread(target=worker) + t.start() + time.sleep(0.001) + t.join(10) + + def test_threaded_leak(self): + gg = [] + def worker(): + # only main greenlet present + gg.append(weakref.ref(greenlet.getcurrent())) + for _ in range(2): + t = threading.Thread(target=worker) + t.start() + t.join(10) + del t + greenlet.getcurrent() # update ts_current + self.__recycle_threads() + greenlet.getcurrent() # update ts_current + gc.collect() + greenlet.getcurrent() # update ts_current + for g in gg: + self.assertIsNone(g()) + + def test_threaded_adv_leak(self): + gg = [] + def worker(): + # main and additional *finished* greenlets + ll = greenlet.getcurrent().ll = [] + def additional(): + ll.append(greenlet.getcurrent()) + for _ in range(2): + greenlet.greenlet(additional).switch() + gg.append(weakref.ref(greenlet.getcurrent())) + for _ in range(2): + t = threading.Thread(target=worker) + t.start() + t.join(10) + del t + greenlet.getcurrent() # update ts_current + self.__recycle_threads() + greenlet.getcurrent() # update ts_current + gc.collect() + greenlet.getcurrent() # update ts_current + for g in gg: + self.assertIsNone(g()) + + def assertClocksUsed(self): + used = greenlet._greenlet.get_clocks_used_doing_optional_cleanup() + self.assertGreaterEqual(used, 0) + # we don't lose the value + greenlet._greenlet.enable_optional_cleanup(True) + used2 = greenlet._greenlet.get_clocks_used_doing_optional_cleanup() + self.assertEqual(used, used2) + self.assertGreater(greenlet._greenlet.CLOCKS_PER_SEC, 1) + + def _check_issue251(self, + manually_collect_background=True, + explicit_reference_to_switch=False): + # See https://github.com/python-greenlet/greenlet/issues/251 + # Killing a greenlet (probably not the main one) + # in one thread from another thread would + # result in leaking a list (the ts_delkey list). + # We no longer use lists to hold that stuff, though. + + # For the test to be valid, even empty lists have to be tracked by the + # GC + + assert gc.is_tracked([]) + HasFinalizerTracksInstances.reset() + greenlet.getcurrent() + greenlets_before = self.count_objects(greenlet.greenlet, exact_kind=False) + + background_glet_running = threading.Event() + background_glet_killed = threading.Event() + background_greenlets = [] + + # XXX: Switching this to a greenlet subclass that overrides + # run results in all callers failing the leaktest; that + # greenlet instance is leaked. There's a bound method for + # run() living on the stack of the greenlet in g_initialstub, + # and since we don't manually switch back to the background + # greenlet to let it "fall off the end" and exit the + # g_initialstub function, it never gets cleaned up. Making the + # garbage collector aware of this bound method (making it an + # attribute of the greenlet structure and traversing into it) + # doesn't help, for some reason. + def background_greenlet(): + # Throw control back to the main greenlet. + jd = HasFinalizerTracksInstances("DELETING STACK OBJECT") + greenlet._greenlet.set_thread_local( + 'test_leaks_key', + HasFinalizerTracksInstances("DELETING THREAD STATE")) + # Explicitly keeping 'switch' in a local variable + # breaks this test in all versions + if explicit_reference_to_switch: + s = greenlet.getcurrent().parent.switch + s([jd]) + else: + greenlet.getcurrent().parent.switch([jd]) + + bg_main_wrefs = [] + + def background_thread(): + glet = greenlet.greenlet(background_greenlet) + bg_main_wrefs.append(weakref.ref(glet.parent)) + + background_greenlets.append(glet) + glet.switch() # Be sure it's active. + # Control is ours again. + del glet # Delete one reference from the thread it runs in. + background_glet_running.set() + background_glet_killed.wait(10) + + # To trigger the background collection of the dead + # greenlet, thus clearing out the contents of the list, we + # need to run some APIs. See issue 252. + if manually_collect_background: + greenlet.getcurrent() + + + t = threading.Thread(target=background_thread) + t.start() + background_glet_running.wait(10) + greenlet.getcurrent() + lists_before = self.count_objects(list, exact_kind=True) + + assert len(background_greenlets) == 1 + self.assertFalse(background_greenlets[0].dead) + # Delete the last reference to the background greenlet + # from a different thread. This puts it in the background thread's + # ts_delkey list. + del background_greenlets[:] + background_glet_killed.set() + + # Now wait for the background thread to die. + t.join(10) + del t + # As part of the fix for 252, we need to cycle the ceval.c + # interpreter loop to be sure it has had a chance to process + # the pending call. + self.wait_for_pending_cleanups() + + lists_after = self.count_objects(list, exact_kind=True) + greenlets_after = self.count_objects(greenlet.greenlet, exact_kind=False) + + # On 2.7, we observe that lists_after is smaller than + # lists_before. No idea what lists got cleaned up. All the + # Python 3 versions match exactly. + self.assertLessEqual(lists_after, lists_before) + # On versions after 3.6, we've successfully cleaned up the + # greenlet references thanks to the internal "vectorcall" + # protocol; prior to that, there is a reference path through + # the ``greenlet.switch`` method still on the stack that we + # can't reach to clean up. The C code goes through terrific + # lengths to clean that up. + if not explicit_reference_to_switch \ + and greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None: + # If cleanup was disabled, though, we may not find it. + self.assertEqual(greenlets_after, greenlets_before) + if manually_collect_background: + # TODO: Figure out how to make this work! + # The one on the stack is still leaking somehow + # in the non-manually-collect state. + self.assertEqual(HasFinalizerTracksInstances.EXTANT_INSTANCES, set()) + else: + # The explicit reference prevents us from collecting it + # and it isn't always found by the GC either for some + # reason. The entire frame is leaked somehow, on some + # platforms (e.g., MacPorts builds of Python (all + # versions!)), but not on other platforms (the linux and + # windows builds on GitHub actions and Appveyor). So we'd + # like to write a test that proves that the main greenlet + # sticks around, and we can on my machine (macOS 11.6, + # MacPorts builds of everything) but we can't write that + # same test on other platforms. However, hopefully iteration + # done by leakcheck will find it. + pass + + if greenlet._greenlet.get_clocks_used_doing_optional_cleanup() is not None: + self.assertClocksUsed() + + def test_issue251_killing_cross_thread_leaks_list(self): + self._check_issue251() + + def test_issue251_with_cleanup_disabled(self): + greenlet._greenlet.enable_optional_cleanup(False) + try: + self._check_issue251() + finally: + greenlet._greenlet.enable_optional_cleanup(True) + + @fails_leakcheck_except_on_free_thraded + def test_issue251_issue252_need_to_collect_in_background(self): + # Between greenlet 1.1.2 and the next version, this was still + # failing because the leak of the list still exists when we + # don't call a greenlet API before exiting the thread. The + # proximate cause is that neither of the two greenlets from + # the background thread are actually being destroyed, even + # though the GC is in fact visiting both objects. It's not + # clear where that leak is? For some reason the thread-local + # dict holding it isn't being cleaned up. + # + # The leak, I think, is in the CPYthon internal function that + # calls into green_switch(). The argument tuple is still on + # the C stack somewhere and can't be reached? That doesn't + # make sense, because the tuple should be collectable when + # this object goes away. + # + # Note that this test sometimes spuriously passes on Linux, + # for some reason, but I've never seen it pass on macOS. + self._check_issue251(manually_collect_background=False) + + @fails_leakcheck_except_on_free_thraded + def test_issue251_issue252_need_to_collect_in_background_cleanup_disabled(self): + self.expect_greenlet_leak = True + greenlet._greenlet.enable_optional_cleanup(False) + try: + self._check_issue251(manually_collect_background=False) + finally: + greenlet._greenlet.enable_optional_cleanup(True) + + @fails_leakcheck_except_on_free_thraded + def test_issue251_issue252_explicit_reference_not_collectable(self): + self._check_issue251( + manually_collect_background=False, + explicit_reference_to_switch=True) + + UNTRACK_ATTEMPTS = 100 + + def _only_test_some_versions(self): + # We're only looking for this problem specifically on 3.11, + # and this set of tests is relatively fragile, depending on + # OS and memory management details. So we want to run it on 3.11+ + # (obviously) but not every older 3.x version in order to reduce + # false negatives. At the moment, those false results seem to have + # resolved, so we are actually running this on 3.8+ + assert sys.version_info[0] >= 3 + if sys.version_info[:2] < (3, 8): + self.skipTest('Only observed on 3.11') + if RUNNING_ON_MANYLINUX: + self.skipTest("Slow and not worth repeating here") + + @ignores_leakcheck + # Because we're just trying to track raw memory, not objects, and running + # the leakcheck makes an already slow test slower. + def test_untracked_memory_doesnt_increase(self): + # See https://github.com/gevent/gevent/issues/1924 + # and https://github.com/python-greenlet/greenlet/issues/328 + self._only_test_some_versions() + def f(): + return 1 + + ITER = 10000 + def run_it(): + for _ in range(ITER): + greenlet.greenlet(f).switch() + + # Establish baseline + for _ in range(3): + run_it() + + # uss: (Linux, macOS, Windows): aka "Unique Set Size", this is + # the memory which is unique to a process and which would be + # freed if the process was terminated right now. + uss_before = self.get_process_uss() + + for count in range(self.UNTRACK_ATTEMPTS): + uss_before = max(uss_before, self.get_process_uss()) + run_it() + + uss_after = self.get_process_uss() + if uss_after <= uss_before and count > 1: + break + + self.assertLessEqual(uss_after, uss_before) + + def _check_untracked_memory_thread(self, deallocate_in_thread=True): + self._only_test_some_versions() + # Like the above test, but what if there are a bunch of + # unfinished greenlets in a thread that dies? + # Does it matter if we deallocate in the thread or not? + EXIT_COUNT = [0] + + def f(): + try: + greenlet.getcurrent().parent.switch() + except greenlet.GreenletExit: + EXIT_COUNT[0] += 1 + raise + return 1 + + ITER = 10000 + def run_it(): + glets = [] + for _ in range(ITER): + # Greenlet starts, switches back to us. + # We keep a strong reference to the greenlet though so it doesn't + # get a GreenletExit exception. + g = greenlet.greenlet(f) + glets.append(g) + g.switch() + + return glets + + test = self + + class ThreadFunc: + uss_before = uss_after = 0 + glets = () + ITER = 2 + def __call__(self): + self.uss_before = test.get_process_uss() + + for _ in range(self.ITER): + self.glets += tuple(run_it()) + + for g in self.glets: + test.assertIn('suspended active', str(g)) + # Drop them. + if deallocate_in_thread: + self.glets = () + self.uss_after = test.get_process_uss() + + # Establish baseline + uss_before = uss_after = None + for count in range(self.UNTRACK_ATTEMPTS): + EXIT_COUNT[0] = 0 + thread_func = ThreadFunc() + t = threading.Thread(target=thread_func) + t.start() + t.join(30) + self.assertFalse(t.is_alive()) + + if uss_before is None: + uss_before = thread_func.uss_before + + uss_before = max(uss_before, thread_func.uss_before) + if deallocate_in_thread: + self.assertEqual(thread_func.glets, ()) + self.assertEqual(EXIT_COUNT[0], ITER * thread_func.ITER) + + del thread_func # Deallocate the greenlets; but this won't raise into them + del t + if not deallocate_in_thread: + self.assertEqual(EXIT_COUNT[0], 0) + if deallocate_in_thread: + self.wait_for_pending_cleanups() + + uss_after = self.get_process_uss() + # See if we achieve a non-growth state at some point. Break when we do. + if uss_after <= uss_before and count > 1: + break + + self.wait_for_pending_cleanups() + uss_after = self.get_process_uss() + self.assertLessEqual(uss_after, uss_before, "after attempts %d" % (count,)) + + @ignores_leakcheck + # Because we're just trying to track raw memory, not objects, and running + # the leakcheck makes an already slow test slower. + def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_thread(self): + self._check_untracked_memory_thread(deallocate_in_thread=True) + + @ignores_leakcheck + # Because the main greenlets from the background threads do not exit in a timely fashion, + # we fail the object-based leakchecks. + def test_untracked_memory_doesnt_increase_unfinished_thread_dealloc_in_main(self): + self._check_untracked_memory_thread(deallocate_in_thread=False) + +if __name__ == '__main__': + __import__('unittest').main() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_stack_saved.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_stack_saved.py new file mode 100644 index 0000000..b362bf9 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_stack_saved.py @@ -0,0 +1,19 @@ +import greenlet +from . import TestCase + + +class Test(TestCase): + + def test_stack_saved(self): + main = greenlet.getcurrent() + self.assertEqual(main._stack_saved, 0) + + def func(): + main.switch(main._stack_saved) + + g = greenlet.greenlet(func) + x = g.switch() + self.assertGreater(x, 0) + self.assertGreater(g._stack_saved, 0) + g.switch() + self.assertEqual(g._stack_saved, 0) diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_throw.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_throw.py new file mode 100644 index 0000000..f4f9a14 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_throw.py @@ -0,0 +1,128 @@ +import sys + + +from greenlet import greenlet +from . import TestCase + +def switch(*args): + return greenlet.getcurrent().parent.switch(*args) + + +class ThrowTests(TestCase): + def test_class(self): + def f(): + try: + switch("ok") + except RuntimeError: + switch("ok") + return + switch("fail") + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw(RuntimeError) + self.assertEqual(res, "ok") + + def test_val(self): + def f(): + try: + switch("ok") + except RuntimeError: + val = sys.exc_info()[1] + if str(val) == "ciao": + switch("ok") + return + switch("fail") + + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw(RuntimeError("ciao")) + self.assertEqual(res, "ok") + + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw(RuntimeError, "ciao") + self.assertEqual(res, "ok") + + def test_kill(self): + def f(): + switch("ok") + switch("fail") + g = greenlet(f) + res = g.switch() + self.assertEqual(res, "ok") + res = g.throw() + self.assertTrue(isinstance(res, greenlet.GreenletExit)) + self.assertTrue(g.dead) + res = g.throw() # immediately eaten by the already-dead greenlet + self.assertTrue(isinstance(res, greenlet.GreenletExit)) + + def test_throw_goes_to_original_parent(self): + main = greenlet.getcurrent() + + def f1(): + try: + main.switch("f1 ready to catch") + except IndexError: + return "caught" + return "normal exit" + + def f2(): + main.switch("from f2") + + g1 = greenlet(f1) + g2 = greenlet(f2, parent=g1) + with self.assertRaises(IndexError): + g2.throw(IndexError) + self.assertTrue(g2.dead) + self.assertTrue(g1.dead) + + g1 = greenlet(f1) + g2 = greenlet(f2, parent=g1) + res = g1.switch() + self.assertEqual(res, "f1 ready to catch") + res = g2.throw(IndexError) + self.assertEqual(res, "caught") + self.assertTrue(g2.dead) + self.assertTrue(g1.dead) + + g1 = greenlet(f1) + g2 = greenlet(f2, parent=g1) + res = g1.switch() + self.assertEqual(res, "f1 ready to catch") + res = g2.switch() + self.assertEqual(res, "from f2") + res = g2.throw(IndexError) + self.assertEqual(res, "caught") + self.assertTrue(g2.dead) + self.assertTrue(g1.dead) + + def test_non_traceback_param(self): + with self.assertRaises(TypeError) as exc: + greenlet.getcurrent().throw( + Exception, + Exception(), + self + ) + self.assertEqual(str(exc.exception), + "throw() third argument must be a traceback object") + + def test_instance_of_wrong_type(self): + with self.assertRaises(TypeError) as exc: + greenlet.getcurrent().throw( + Exception(), + BaseException() + ) + + self.assertEqual(str(exc.exception), + "instance exception may not have a separate value") + + def test_not_throwable(self): + with self.assertRaises(TypeError) as exc: + greenlet.getcurrent().throw( + "abc" + ) + self.assertEqual(str(exc.exception), + "exceptions must be classes, or instances, not str") diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_tracing.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_tracing.py new file mode 100644 index 0000000..235fbcd --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_tracing.py @@ -0,0 +1,299 @@ +from __future__ import print_function +import sys +import sysconfig +import greenlet +import unittest + +from . import TestCase +from . import PY312 + +# https://discuss.python.org/t/cpython-3-12-greenlet-and-tracing-profiling-how-to-not-crash-and-get-correct-results/33144/2 +# When build variables are available, OPT is the best way of detecting +# the build with assertions enabled. Otherwise, fallback to detecting PyDEBUG +# build. +ASSERTION_BUILD_PY312 = ( + PY312 and ( + "-DNDEBUG" not in sysconfig.get_config_var("OPT").split() + if sysconfig.get_config_var("OPT") is not None + else hasattr(sys, 'gettotalrefcount') + ), + "Broken on assertion-enabled builds of Python 3.12" +) + +class SomeError(Exception): + pass + +class GreenletTracer(object): + oldtrace = None + + def __init__(self, error_on_trace=False): + self.actions = [] + self.error_on_trace = error_on_trace + + def __call__(self, *args): + self.actions.append(args) + if self.error_on_trace: + raise SomeError + + def __enter__(self): + self.oldtrace = greenlet.settrace(self) + return self.actions + + def __exit__(self, *args): + greenlet.settrace(self.oldtrace) + + +class TestGreenletTracing(TestCase): + """ + Tests of ``greenlet.settrace()`` + """ + + def test_a_greenlet_tracing(self): + main = greenlet.getcurrent() + def dummy(): + pass + def dummyexc(): + raise SomeError() + + with GreenletTracer() as actions: + g1 = greenlet.greenlet(dummy) + g1.switch() + g2 = greenlet.greenlet(dummyexc) + self.assertRaises(SomeError, g2.switch) + + self.assertEqual(actions, [ + ('switch', (main, g1)), + ('switch', (g1, main)), + ('switch', (main, g2)), + ('throw', (g2, main)), + ]) + + def test_b_exception_disables_tracing(self): + main = greenlet.getcurrent() + def dummy(): + main.switch() + g = greenlet.greenlet(dummy) + g.switch() + with GreenletTracer(error_on_trace=True) as actions: + self.assertRaises(SomeError, g.switch) + self.assertEqual(greenlet.gettrace(), None) + + self.assertEqual(actions, [ + ('switch', (main, g)), + ]) + + def test_set_same_tracer_twice(self): + # https://github.com/python-greenlet/greenlet/issues/332 + # Our logic in asserting that the tracefunction should + # gain a reference was incorrect if the same tracefunction was set + # twice. + tracer = GreenletTracer() + with tracer: + greenlet.settrace(tracer) + + +class PythonTracer(object): + oldtrace = None + + def __init__(self): + self.actions = [] + + def __call__(self, frame, event, arg): + # Record the co_name so we have an idea what function we're in. + self.actions.append((event, frame.f_code.co_name)) + + def __enter__(self): + self.oldtrace = sys.setprofile(self) + return self.actions + + def __exit__(self, *args): + sys.setprofile(self.oldtrace) + +def tpt_callback(): + return 42 + +class TestPythonTracing(TestCase): + """ + Tests of the interaction of ``sys.settrace()`` + with greenlet facilities. + + NOTE: Most of this is probably CPython specific. + """ + + maxDiff = None + + def test_trace_events_trivial(self): + with PythonTracer() as actions: + tpt_callback() + # If we use the sys.settrace instead of setprofile, we get + # this: + + # self.assertEqual(actions, [ + # ('call', 'tpt_callback'), + # ('call', '__exit__'), + # ]) + + self.assertEqual(actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + def _trace_switch(self, glet): + with PythonTracer() as actions: + glet.switch() + return actions + + def _check_trace_events_func_already_set(self, glet): + actions = self._trace_switch(glet) + self.assertEqual(actions, [ + ('return', '__enter__'), + ('c_call', '_trace_switch'), + ('call', 'run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('return', 'run'), + ('c_return', '_trace_switch'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + def test_trace_events_into_greenlet_func_already_set(self): + def run(): + return tpt_callback() + + self._check_trace_events_func_already_set(greenlet.greenlet(run)) + + def test_trace_events_into_greenlet_subclass_already_set(self): + class X(greenlet.greenlet): + def run(self): + return tpt_callback() + self._check_trace_events_func_already_set(X()) + + def _check_trace_events_from_greenlet_sets_profiler(self, g, tracer): + g.switch() + tpt_callback() + tracer.__exit__() + self.assertEqual(tracer.actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('return', 'run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + + def test_trace_events_from_greenlet_func_sets_profiler(self): + tracer = PythonTracer() + def run(): + tracer.__enter__() + return tpt_callback() + + self._check_trace_events_from_greenlet_sets_profiler(greenlet.greenlet(run), + tracer) + + def test_trace_events_from_greenlet_subclass_sets_profiler(self): + tracer = PythonTracer() + class X(greenlet.greenlet): + def run(self): + tracer.__enter__() + return tpt_callback() + + self._check_trace_events_from_greenlet_sets_profiler(X(), tracer) + + @unittest.skipIf(*ASSERTION_BUILD_PY312) + def test_trace_events_multiple_greenlets_switching(self): + tracer = PythonTracer() + + g1 = None + g2 = None + + def g1_run(): + tracer.__enter__() + tpt_callback() + g2.switch() + tpt_callback() + return 42 + + def g2_run(): + tpt_callback() + tracer.__exit__() + tpt_callback() + g1.switch() + + g1 = greenlet.greenlet(g1_run) + g2 = greenlet.greenlet(g2_run) + + x = g1.switch() + self.assertEqual(x, 42) + tpt_callback() # ensure not in the trace + self.assertEqual(tracer.actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('c_call', 'g1_run'), + ('call', 'g2_run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + @unittest.skipIf(*ASSERTION_BUILD_PY312) + def test_trace_events_multiple_greenlets_switching_siblings(self): + # Like the first version, but get both greenlets running first + # as "siblings" and then establish the tracing. + tracer = PythonTracer() + + g1 = None + g2 = None + + def g1_run(): + greenlet.getcurrent().parent.switch() + tracer.__enter__() + tpt_callback() + g2.switch() + tpt_callback() + return 42 + + def g2_run(): + greenlet.getcurrent().parent.switch() + + tpt_callback() + tracer.__exit__() + tpt_callback() + g1.switch() + + g1 = greenlet.greenlet(g1_run) + g2 = greenlet.greenlet(g2_run) + + # Start g1 + g1.switch() + # And it immediately returns control to us. + # Start g2 + g2.switch() + # Which also returns. Now kick of the real part of the + # test. + x = g1.switch() + self.assertEqual(x, 42) + + tpt_callback() # ensure not in the trace + self.assertEqual(tracer.actions, [ + ('return', '__enter__'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('c_call', 'g1_run'), + ('call', 'tpt_callback'), + ('return', 'tpt_callback'), + ('call', '__exit__'), + ('c_call', '__exit__'), + ]) + + +if __name__ == '__main__': + unittest.main() diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_version.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_version.py new file mode 100644 index 0000000..96c17cf --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_version.py @@ -0,0 +1,41 @@ +#! /usr/bin/env python +from __future__ import absolute_import +from __future__ import print_function + +import sys +import os +from unittest import TestCase as NonLeakingTestCase + +import greenlet + +# No reason to run this multiple times under leakchecks, +# it doesn't do anything. +class VersionTests(NonLeakingTestCase): + def test_version(self): + def find_dominating_file(name): + if os.path.exists(name): + return name + + tried = [] + here = os.path.abspath(os.path.dirname(__file__)) + for i in range(10): + up = ['..'] * i + path = [here] + up + [name] + fname = os.path.join(*path) + fname = os.path.abspath(fname) + tried.append(fname) + if os.path.exists(fname): + return fname + raise AssertionError("Could not find file " + name + "; checked " + str(tried)) + + try: + setup_py = find_dominating_file('setup.py') + except AssertionError as e: + self.skipTest("Unable to find setup.py; must be out of tree. " + str(e)) + + + invoke_setup = "%s %s --version" % (sys.executable, setup_py) + with os.popen(invoke_setup) as f: + sversion = f.read().strip() + + self.assertEqual(sversion, greenlet.__version__) diff --git a/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_weakref.py b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_weakref.py new file mode 100644 index 0000000..05a38a7 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/greenlet/tests/test_weakref.py @@ -0,0 +1,35 @@ +import gc +import weakref + + +import greenlet +from . import TestCase + +class WeakRefTests(TestCase): + def test_dead_weakref(self): + def _dead_greenlet(): + g = greenlet.greenlet(lambda: None) + g.switch() + return g + o = weakref.ref(_dead_greenlet()) + gc.collect() + self.assertEqual(o(), None) + + def test_inactive_weakref(self): + o = weakref.ref(greenlet.greenlet()) + gc.collect() + self.assertEqual(o(), None) + + def test_dealloc_weakref(self): + seen = [] + def worker(): + try: + greenlet.getcurrent().parent.switch() + finally: + seen.append(g()) + g = greenlet.greenlet(worker) + g.switch() + g2 = greenlet.greenlet(lambda: None, g) + g = weakref.ref(g2) + g2 = None + self.assertEqual(seen, [None]) diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/INSTALLER b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/LICENSE b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/LICENSE new file mode 100644 index 0000000..0dca6e1 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/LICENSE @@ -0,0 +1,23 @@ +2009-2024 (c) Benoît Chesneau +2009-2015 (c) Paul J. Davis + +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/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/METADATA b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/METADATA new file mode 100644 index 0000000..550aef2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/METADATA @@ -0,0 +1,130 @@ +Metadata-Version: 2.1 +Name: gunicorn +Version: 23.0.0 +Summary: WSGI HTTP Server for UNIX +Author-email: Benoit Chesneau +License: MIT +Project-URL: Homepage, https://gunicorn.org +Project-URL: Documentation, https://docs.gunicorn.org +Project-URL: Issue tracker, https://github.com/benoitc/gunicorn/issues +Project-URL: Source code, https://github.com/benoitc/gunicorn +Project-URL: Changelog, https://docs.gunicorn.org/en/stable/news.html +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Other Environment +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: MacOS :: MacOS X +Classifier: Operating System :: POSIX +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +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 :: 3.12 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Topic :: Internet +Classifier: Topic :: Utilities +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI +Classifier: Topic :: Internet :: WWW/HTTP :: WSGI :: Server +Classifier: Topic :: Internet :: WWW/HTTP :: Dynamic Content +Requires-Python: >=3.7 +Description-Content-Type: text/x-rst +License-File: LICENSE +Requires-Dist: packaging +Requires-Dist: importlib-metadata ; python_version < "3.8" +Provides-Extra: eventlet +Requires-Dist: eventlet !=0.36.0,>=0.24.1 ; extra == 'eventlet' +Provides-Extra: gevent +Requires-Dist: gevent >=1.4.0 ; extra == 'gevent' +Provides-Extra: gthread +Provides-Extra: setproctitle +Requires-Dist: setproctitle ; extra == 'setproctitle' +Provides-Extra: testing +Requires-Dist: gevent ; extra == 'testing' +Requires-Dist: eventlet ; extra == 'testing' +Requires-Dist: coverage ; extra == 'testing' +Requires-Dist: pytest ; extra == 'testing' +Requires-Dist: pytest-cov ; extra == 'testing' +Provides-Extra: tornado +Requires-Dist: tornado >=0.2 ; extra == 'tornado' + +Gunicorn +-------- + +.. image:: https://img.shields.io/pypi/v/gunicorn.svg?style=flat + :alt: PyPI version + :target: https://pypi.python.org/pypi/gunicorn + +.. image:: https://img.shields.io/pypi/pyversions/gunicorn.svg + :alt: Supported Python versions + :target: https://pypi.python.org/pypi/gunicorn + +.. image:: https://github.com/benoitc/gunicorn/actions/workflows/tox.yml/badge.svg + :alt: Build Status + :target: https://github.com/benoitc/gunicorn/actions/workflows/tox.yml + +.. image:: https://github.com/benoitc/gunicorn/actions/workflows/lint.yml/badge.svg + :alt: Lint Status + :target: https://github.com/benoitc/gunicorn/actions/workflows/lint.yml + +Gunicorn 'Green Unicorn' is a Python WSGI HTTP Server for UNIX. It's a pre-fork +worker model ported from Ruby's Unicorn_ project. The Gunicorn server is broadly +compatible with various web frameworks, simply implemented, light on server +resource usage, and fairly speedy. + +Feel free to join us in `#gunicorn`_ on `Libera.chat`_. + +Documentation +------------- + +The documentation is hosted at https://docs.gunicorn.org. + +Installation +------------ + +Gunicorn requires **Python 3.x >= 3.7**. + +Install from PyPI:: + + $ pip install gunicorn + + +Usage +----- + +Basic usage:: + + $ gunicorn [OPTIONS] APP_MODULE + +Where ``APP_MODULE`` is of the pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``. The +module name can be a full dotted path. The variable name refers to a WSGI +callable that should be found in the specified module. + +Example with test app:: + + $ cd examples + $ gunicorn --workers=2 test:app + + +Contributing +------------ + +See `our complete contributor's guide `_ for more details. + + +License +------- + +Gunicorn is released under the MIT License. See the LICENSE_ file for more +details. + +.. _Unicorn: https://bogomips.org/unicorn/ +.. _`#gunicorn`: https://web.libera.chat/?channels=#gunicorn +.. _`Libera.chat`: https://libera.chat/ +.. _LICENSE: https://github.com/benoitc/gunicorn/blob/master/LICENSE diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/RECORD b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/RECORD new file mode 100644 index 0000000..a0e0895 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/RECORD @@ -0,0 +1,77 @@ +../../../bin/gunicorn,sha256=tukOdMUWszujoVr9ctAuI5EinVaq3Sw2nFO_0_9UVYw,241 +gunicorn-23.0.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +gunicorn-23.0.0.dist-info/LICENSE,sha256=ZkbNu6LpnjQh3RjCIXNXmh_eNH6DHa5q3ugO7-Mx6VE,1136 +gunicorn-23.0.0.dist-info/METADATA,sha256=KhY-mRcAcWCLIbXIHihsUNKWB5fGDOrsbq-JKQTBHY4,4421 +gunicorn-23.0.0.dist-info/RECORD,, +gunicorn-23.0.0.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +gunicorn-23.0.0.dist-info/WHEEL,sha256=R0nc6qTxuoLk7ShA2_Y-UWkN8ZdfDBG2B6Eqpz2WXbs,91 +gunicorn-23.0.0.dist-info/entry_points.txt,sha256=bF8VNiG4H8W83JfEBcqcPMydv9hl04CS4kwh1KOYrFY,113 +gunicorn-23.0.0.dist-info/top_level.txt,sha256=cdMaa2yhxb8do-WioY9qRHUCfwf55YztjwQCncaInoE,9 +gunicorn/__init__.py,sha256=NaLW_JTiKLgqMXipjqzxFn-1wdiptlO2WxOB_KKwx94,257 +gunicorn/__main__.py,sha256=tviepyuwKyB6SPV28t2eZy_5PcCpT56z7QZjzbMpkQw,338 +gunicorn/__pycache__/__init__.cpython-311.pyc,, +gunicorn/__pycache__/__main__.cpython-311.pyc,, +gunicorn/__pycache__/arbiter.cpython-311.pyc,, +gunicorn/__pycache__/config.cpython-311.pyc,, +gunicorn/__pycache__/debug.cpython-311.pyc,, +gunicorn/__pycache__/errors.cpython-311.pyc,, +gunicorn/__pycache__/glogging.cpython-311.pyc,, +gunicorn/__pycache__/pidfile.cpython-311.pyc,, +gunicorn/__pycache__/reloader.cpython-311.pyc,, +gunicorn/__pycache__/sock.cpython-311.pyc,, +gunicorn/__pycache__/systemd.cpython-311.pyc,, +gunicorn/__pycache__/util.cpython-311.pyc,, +gunicorn/app/__init__.py,sha256=8m9lIbhRssnbGuBeQUA-vNSNbMeNju9Q_PUnnNfqOYU,105 +gunicorn/app/__pycache__/__init__.cpython-311.pyc,, +gunicorn/app/__pycache__/base.cpython-311.pyc,, +gunicorn/app/__pycache__/pasterapp.cpython-311.pyc,, +gunicorn/app/__pycache__/wsgiapp.cpython-311.pyc,, +gunicorn/app/base.py,sha256=KV2aIO50JTlakHL82q9zu3LhCJrDmUmaViwSy14Gk6U,7370 +gunicorn/app/pasterapp.py,sha256=BIa0mz_J86NuObUw2UIyjLYKUm8V3b034pJrTkvF-sA,2016 +gunicorn/app/wsgiapp.py,sha256=gVBgUc_3uSK0QzXYQ1XbutacEGjf44CgxAaYkgwfucY,1924 +gunicorn/arbiter.py,sha256=xcHpv8bsrYpIpu9q7YK4ue11f9kmz80dr7BUwKX3oxk,21470 +gunicorn/config.py,sha256=t3BChwMoBZwfV05Iy_n3oh232xvi1SORkOJfHFL_c-8,70318 +gunicorn/debug.py,sha256=c8cQv_g3d22JE6A4hv7FNmMhm4wq6iB_E-toorpqJcw,2263 +gunicorn/errors.py,sha256=iLTJQC4SVSRoygIGGHXvEp0d8UdzpeqmMRqUcF0JI14,897 +gunicorn/glogging.py,sha256=76MlUUc82FqdeD3R4qC8NeUHt8vxa3IBSxmeBtbZKtE,15273 +gunicorn/http/__init__.py,sha256=1k_WWvjT9eDDRDOutzXCebvYKm_qzaQA3GuLk0VkbJI,255 +gunicorn/http/__pycache__/__init__.cpython-311.pyc,, +gunicorn/http/__pycache__/body.cpython-311.pyc,, +gunicorn/http/__pycache__/errors.cpython-311.pyc,, +gunicorn/http/__pycache__/message.cpython-311.pyc,, +gunicorn/http/__pycache__/parser.cpython-311.pyc,, +gunicorn/http/__pycache__/unreader.cpython-311.pyc,, +gunicorn/http/__pycache__/wsgi.cpython-311.pyc,, +gunicorn/http/body.py,sha256=sQgp_hJUjx8DK6LYzklMTl-xKcX8efsbreCKzowCGmo,7600 +gunicorn/http/errors.py,sha256=6tcG9pCvRiooXpfudQBILzUPx3ertuQ5utjZeUNMUqA,3437 +gunicorn/http/message.py,sha256=ok4xnqWhntIn21gcPa1KYZWRYTbwsECpot-Eac47qFs,17632 +gunicorn/http/parser.py,sha256=wayoAFjQYERSwE4YGwI2AYSNGZ2eTNbGUtoqqQFph5U,1334 +gunicorn/http/unreader.py,sha256=D7bluz62A1aLZQ9XbpX0-nDBal9KPtp_pjokk2YNY8E,1913 +gunicorn/http/wsgi.py,sha256=x-zTT7gvRF4wipmvoVePz1qO407JZCU_sNU8yjcl_R4,12811 +gunicorn/instrument/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +gunicorn/instrument/__pycache__/__init__.cpython-311.pyc,, +gunicorn/instrument/__pycache__/statsd.cpython-311.pyc,, +gunicorn/instrument/statsd.py,sha256=ghmaniNEjMMLvvdQkDPpB_u9a8z4FBfWUE_C9O1KIYQ,4750 +gunicorn/pidfile.py,sha256=HntiveG8eJmwB8_D3o5cBXRuGKnC0cvWxg90MWh1hUc,2327 +gunicorn/reloader.py,sha256=oDuK2PWGyIMm0_vc1y196Z1EggOvBi-Iz_2UbRY7PsQ,3761 +gunicorn/sock.py,sha256=VVF2eeoxQEJ2OEoZoek3BFZTqj7wXvQql7jpdFAjVTI,6834 +gunicorn/systemd.py,sha256=DmWbcqeRyHdAIy70UCEg2J93v6PpESp3EFTNm0Djgyg,2498 +gunicorn/util.py,sha256=YqC4E3RxhFNH-W4LOqy1RtxcHRy9hRyYND92ZSNXEwc,19095 +gunicorn/workers/__init__.py,sha256=Y0Z6WhXKY6PuTbFkOkeEBzIfhDDg5FeqVg8aJp6lIZA,572 +gunicorn/workers/__pycache__/__init__.cpython-311.pyc,, +gunicorn/workers/__pycache__/base.cpython-311.pyc,, +gunicorn/workers/__pycache__/base_async.cpython-311.pyc,, +gunicorn/workers/__pycache__/geventlet.cpython-311.pyc,, +gunicorn/workers/__pycache__/ggevent.cpython-311.pyc,, +gunicorn/workers/__pycache__/gthread.cpython-311.pyc,, +gunicorn/workers/__pycache__/gtornado.cpython-311.pyc,, +gunicorn/workers/__pycache__/sync.cpython-311.pyc,, +gunicorn/workers/__pycache__/workertmp.cpython-311.pyc,, +gunicorn/workers/base.py,sha256=eM9MTLP9PdWL0Pm5V5byyBli-r8zF2MSEGjefr3y92M,9763 +gunicorn/workers/base_async.py,sha256=Oc-rSV81uHqvEqww2PM6tz75qNR07ChuqM6IkTOpzlk,5627 +gunicorn/workers/geventlet.py,sha256=s_I-gKYgDJnlAHdCxN_wfglODnDE1eJaZJZCJyNYg-4,6069 +gunicorn/workers/ggevent.py,sha256=OEhj-bFVBGQ-jbjr5S3gSvixJTa-YOQYht7fYTOCyt4,6030 +gunicorn/workers/gthread.py,sha256=moycCQoJS602u3U7gZEooYxqRP86Tq5bmQnipL4a4_c,12500 +gunicorn/workers/gtornado.py,sha256=zCHbxs5JeE9rtZa5mXlhftBlNlwp_tBWXuTQwqgv1so,5811 +gunicorn/workers/sync.py,sha256=mOY84VHbAx62lmo2DLuifkK9d6anEgvC7LAuYVJyRM4,7204 +gunicorn/workers/workertmp.py,sha256=bswGosCIDb_wBfdGaFqHopgxbmJ6rgVXYlVhJDWZKIc,1604 diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/REQUESTED b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/WHEEL b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/WHEEL new file mode 100644 index 0000000..1a9c535 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (72.1.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/entry_points.txt b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/entry_points.txt new file mode 100644 index 0000000..fd14749 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/entry_points.txt @@ -0,0 +1,5 @@ +[console_scripts] +gunicorn = gunicorn.app.wsgiapp:run + +[paste.server_runner] +main = gunicorn.app.pasterapp:serve diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/top_level.txt b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/top_level.txt new file mode 100644 index 0000000..8f22dcc --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +gunicorn diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/__init__.py b/netdeploy/lib/python3.11/site-packages/gunicorn/__init__.py new file mode 100644 index 0000000..cdcd135 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/__init__.py @@ -0,0 +1,8 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +version_info = (23, 0, 0) +__version__ = ".".join([str(v) for v in version_info]) +SERVER = "gunicorn" +SERVER_SOFTWARE = "%s/%s" % (SERVER, __version__) diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/__main__.py b/netdeploy/lib/python3.11/site-packages/gunicorn/__main__.py new file mode 100644 index 0000000..ceb44d0 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/__main__.py @@ -0,0 +1,10 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +from gunicorn.app.wsgiapp import run + +if __name__ == "__main__": + # see config.py - argparse defaults to basename(argv[0]) == "__main__.py" + # todo: let runpy.run_module take care of argv[0] rewriting + run(prog="gunicorn") diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/app/__init__.py b/netdeploy/lib/python3.11/site-packages/gunicorn/app/__init__.py new file mode 100644 index 0000000..530e35c --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/app/__init__.py @@ -0,0 +1,3 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/app/base.py b/netdeploy/lib/python3.11/site-packages/gunicorn/app/base.py new file mode 100644 index 0000000..9bf7a4f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/app/base.py @@ -0,0 +1,235 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. +import importlib.util +import importlib.machinery +import os +import sys +import traceback + +from gunicorn import util +from gunicorn.arbiter import Arbiter +from gunicorn.config import Config, get_default_config_file +from gunicorn import debug + + +class BaseApplication: + """ + An application interface for configuring and loading + the various necessities for any given web framework. + """ + def __init__(self, usage=None, prog=None): + self.usage = usage + self.cfg = None + self.callable = None + self.prog = prog + self.logger = None + self.do_load_config() + + def do_load_config(self): + """ + Loads the configuration + """ + try: + self.load_default_config() + self.load_config() + except Exception as e: + print("\nError: %s" % str(e), file=sys.stderr) + sys.stderr.flush() + sys.exit(1) + + def load_default_config(self): + # init configuration + self.cfg = Config(self.usage, prog=self.prog) + + def init(self, parser, opts, args): + raise NotImplementedError + + def load(self): + raise NotImplementedError + + def load_config(self): + """ + This method is used to load the configuration from one or several input(s). + Custom Command line, configuration file. + You have to override this method in your class. + """ + raise NotImplementedError + + def reload(self): + self.do_load_config() + if self.cfg.spew: + debug.spew() + + def wsgi(self): + if self.callable is None: + self.callable = self.load() + return self.callable + + def run(self): + try: + Arbiter(self).run() + except RuntimeError as e: + print("\nError: %s\n" % e, file=sys.stderr) + sys.stderr.flush() + sys.exit(1) + + +class Application(BaseApplication): + + # 'init' and 'load' methods are implemented by WSGIApplication. + # pylint: disable=abstract-method + + def chdir(self): + # chdir to the configured path before loading, + # default is the current dir + os.chdir(self.cfg.chdir) + + # add the path to sys.path + if self.cfg.chdir not in sys.path: + sys.path.insert(0, self.cfg.chdir) + + def get_config_from_filename(self, filename): + + if not os.path.exists(filename): + raise RuntimeError("%r doesn't exist" % filename) + + ext = os.path.splitext(filename)[1] + + try: + module_name = '__config__' + if ext in [".py", ".pyc"]: + spec = importlib.util.spec_from_file_location(module_name, filename) + else: + msg = "configuration file should have a valid Python extension.\n" + util.warn(msg) + loader_ = importlib.machinery.SourceFileLoader(module_name, filename) + spec = importlib.util.spec_from_file_location(module_name, filename, loader=loader_) + mod = importlib.util.module_from_spec(spec) + sys.modules[module_name] = mod + spec.loader.exec_module(mod) + except Exception: + print("Failed to read config file: %s" % filename, file=sys.stderr) + traceback.print_exc() + sys.stderr.flush() + sys.exit(1) + + return vars(mod) + + def get_config_from_module_name(self, module_name): + return vars(importlib.import_module(module_name)) + + def load_config_from_module_name_or_filename(self, location): + """ + Loads the configuration file: the file is a python file, otherwise raise an RuntimeError + Exception or stop the process if the configuration file contains a syntax error. + """ + + if location.startswith("python:"): + module_name = location[len("python:"):] + cfg = self.get_config_from_module_name(module_name) + else: + if location.startswith("file:"): + filename = location[len("file:"):] + else: + filename = location + cfg = self.get_config_from_filename(filename) + + for k, v in cfg.items(): + # Ignore unknown names + if k not in self.cfg.settings: + continue + try: + self.cfg.set(k.lower(), v) + except Exception: + print("Invalid value for %s: %s\n" % (k, v), file=sys.stderr) + sys.stderr.flush() + raise + + return cfg + + def load_config_from_file(self, filename): + return self.load_config_from_module_name_or_filename(location=filename) + + def load_config(self): + # parse console args + parser = self.cfg.parser() + args = parser.parse_args() + + # optional settings from apps + cfg = self.init(parser, args, args.args) + + # set up import paths and follow symlinks + self.chdir() + + # Load up the any app specific configuration + if cfg: + for k, v in cfg.items(): + self.cfg.set(k.lower(), v) + + env_args = parser.parse_args(self.cfg.get_cmd_args_from_env()) + + if args.config: + self.load_config_from_file(args.config) + elif env_args.config: + self.load_config_from_file(env_args.config) + else: + default_config = get_default_config_file() + if default_config is not None: + self.load_config_from_file(default_config) + + # Load up environment configuration + for k, v in vars(env_args).items(): + if v is None: + continue + if k == "args": + continue + self.cfg.set(k.lower(), v) + + # Lastly, update the configuration with any command line settings. + for k, v in vars(args).items(): + if v is None: + continue + if k == "args": + continue + self.cfg.set(k.lower(), v) + + # current directory might be changed by the config now + # set up import paths and follow symlinks + self.chdir() + + def run(self): + if self.cfg.print_config: + print(self.cfg) + + if self.cfg.print_config or self.cfg.check_config: + try: + self.load() + except Exception: + msg = "\nError while loading the application:\n" + print(msg, file=sys.stderr) + traceback.print_exc() + sys.stderr.flush() + sys.exit(1) + sys.exit(0) + + if self.cfg.spew: + debug.spew() + + if self.cfg.daemon: + if os.environ.get('NOTIFY_SOCKET'): + msg = "Warning: you shouldn't specify `daemon = True`" \ + " when launching by systemd with `Type = notify`" + print(msg, file=sys.stderr, flush=True) + + util.daemonize(self.cfg.enable_stdio_inheritance) + + # set python paths + if self.cfg.pythonpath: + paths = self.cfg.pythonpath.split(",") + for path in paths: + pythonpath = os.path.abspath(path) + if pythonpath not in sys.path: + sys.path.insert(0, pythonpath) + + super().run() diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/app/pasterapp.py b/netdeploy/lib/python3.11/site-packages/gunicorn/app/pasterapp.py new file mode 100644 index 0000000..b1738f2 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/app/pasterapp.py @@ -0,0 +1,74 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import configparser +import os + +from paste.deploy import loadapp + +from gunicorn.app.wsgiapp import WSGIApplication +from gunicorn.config import get_default_config_file + + +def get_wsgi_app(config_uri, name=None, defaults=None): + if ':' not in config_uri: + config_uri = "config:%s" % config_uri + + return loadapp( + config_uri, + name=name, + relative_to=os.getcwd(), + global_conf=defaults, + ) + + +def has_logging_config(config_file): + parser = configparser.ConfigParser() + parser.read([config_file]) + return parser.has_section('loggers') + + +def serve(app, global_conf, **local_conf): + """\ + A Paste Deployment server runner. + + Example configuration: + + [server:main] + use = egg:gunicorn#main + host = 127.0.0.1 + port = 5000 + """ + config_file = global_conf['__file__'] + gunicorn_config_file = local_conf.pop('config', None) + + host = local_conf.pop('host', '') + port = local_conf.pop('port', '') + if host and port: + local_conf['bind'] = '%s:%s' % (host, port) + elif host: + local_conf['bind'] = host.split(',') + + class PasterServerApplication(WSGIApplication): + def load_config(self): + self.cfg.set("default_proc_name", config_file) + + if has_logging_config(config_file): + self.cfg.set("logconfig", config_file) + + if gunicorn_config_file: + self.load_config_from_file(gunicorn_config_file) + else: + default_gunicorn_config_file = get_default_config_file() + if default_gunicorn_config_file is not None: + self.load_config_from_file(default_gunicorn_config_file) + + for k, v in local_conf.items(): + if v is not None: + self.cfg.set(k.lower(), v) + + def load(self): + return app + + PasterServerApplication().run() diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/app/wsgiapp.py b/netdeploy/lib/python3.11/site-packages/gunicorn/app/wsgiapp.py new file mode 100644 index 0000000..1b0ba96 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/app/wsgiapp.py @@ -0,0 +1,70 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import os + +from gunicorn.errors import ConfigError +from gunicorn.app.base import Application +from gunicorn import util + + +class WSGIApplication(Application): + def init(self, parser, opts, args): + self.app_uri = None + + if opts.paste: + from .pasterapp import has_logging_config + + config_uri = os.path.abspath(opts.paste) + config_file = config_uri.split('#')[0] + + if not os.path.exists(config_file): + raise ConfigError("%r not found" % config_file) + + self.cfg.set("default_proc_name", config_file) + self.app_uri = config_uri + + if has_logging_config(config_file): + self.cfg.set("logconfig", config_file) + + return + + if len(args) > 0: + self.cfg.set("default_proc_name", args[0]) + self.app_uri = args[0] + + def load_config(self): + super().load_config() + + if self.app_uri is None: + if self.cfg.wsgi_app is not None: + self.app_uri = self.cfg.wsgi_app + else: + raise ConfigError("No application module specified.") + + def load_wsgiapp(self): + return util.import_app(self.app_uri) + + def load_pasteapp(self): + from .pasterapp import get_wsgi_app + return get_wsgi_app(self.app_uri, defaults=self.cfg.paste_global_conf) + + def load(self): + if self.cfg.paste is not None: + return self.load_pasteapp() + else: + return self.load_wsgiapp() + + +def run(prog=None): + """\ + The ``gunicorn`` command line runner for launching Gunicorn with + generic WSGI applications. + """ + from gunicorn.app.wsgiapp import WSGIApplication + WSGIApplication("%(prog)s [OPTIONS] [APP_MODULE]", prog=prog).run() + + +if __name__ == '__main__': + run() diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/arbiter.py b/netdeploy/lib/python3.11/site-packages/gunicorn/arbiter.py new file mode 100644 index 0000000..1eaf453 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/arbiter.py @@ -0,0 +1,671 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. +import errno +import os +import random +import select +import signal +import sys +import time +import traceback + +from gunicorn.errors import HaltServer, AppImportError +from gunicorn.pidfile import Pidfile +from gunicorn import sock, systemd, util + +from gunicorn import __version__, SERVER_SOFTWARE + + +class Arbiter: + """ + Arbiter maintain the workers processes alive. It launches or + kills them if needed. It also manages application reloading + via SIGHUP/USR2. + """ + + # A flag indicating if a worker failed to + # to boot. If a worker process exist with + # this error code, the arbiter will terminate. + WORKER_BOOT_ERROR = 3 + + # A flag indicating if an application failed to be loaded + APP_LOAD_ERROR = 4 + + START_CTX = {} + + LISTENERS = [] + WORKERS = {} + PIPE = [] + + # I love dynamic languages + SIG_QUEUE = [] + SIGNALS = [getattr(signal, "SIG%s" % x) + for x in "HUP QUIT INT TERM TTIN TTOU USR1 USR2 WINCH".split()] + SIG_NAMES = dict( + (getattr(signal, name), name[3:].lower()) for name in dir(signal) + if name[:3] == "SIG" and name[3] != "_" + ) + + def __init__(self, app): + os.environ["SERVER_SOFTWARE"] = SERVER_SOFTWARE + + self._num_workers = None + self._last_logged_active_worker_count = None + self.log = None + + self.setup(app) + + self.pidfile = None + self.systemd = False + self.worker_age = 0 + self.reexec_pid = 0 + self.master_pid = 0 + self.master_name = "Master" + + cwd = util.getcwd() + + args = sys.argv[:] + args.insert(0, sys.executable) + + # init start context + self.START_CTX = { + "args": args, + "cwd": cwd, + 0: sys.executable + } + + def _get_num_workers(self): + return self._num_workers + + def _set_num_workers(self, value): + old_value = self._num_workers + self._num_workers = value + self.cfg.nworkers_changed(self, value, old_value) + num_workers = property(_get_num_workers, _set_num_workers) + + def setup(self, app): + self.app = app + self.cfg = app.cfg + + if self.log is None: + self.log = self.cfg.logger_class(app.cfg) + + # reopen files + if 'GUNICORN_PID' in os.environ: + self.log.reopen_files() + + self.worker_class = self.cfg.worker_class + self.address = self.cfg.address + self.num_workers = self.cfg.workers + self.timeout = self.cfg.timeout + self.proc_name = self.cfg.proc_name + + self.log.debug('Current configuration:\n{0}'.format( + '\n'.join( + ' {0}: {1}'.format(config, value.value) + for config, value + in sorted(self.cfg.settings.items(), + key=lambda setting: setting[1])))) + + # set environment' variables + if self.cfg.env: + for k, v in self.cfg.env.items(): + os.environ[k] = v + + if self.cfg.preload_app: + self.app.wsgi() + + def start(self): + """\ + Initialize the arbiter. Start listening and set pidfile if needed. + """ + self.log.info("Starting gunicorn %s", __version__) + + if 'GUNICORN_PID' in os.environ: + self.master_pid = int(os.environ.get('GUNICORN_PID')) + self.proc_name = self.proc_name + ".2" + self.master_name = "Master.2" + + self.pid = os.getpid() + if self.cfg.pidfile is not None: + pidname = self.cfg.pidfile + if self.master_pid != 0: + pidname += ".2" + self.pidfile = Pidfile(pidname) + self.pidfile.create(self.pid) + self.cfg.on_starting(self) + + self.init_signals() + + if not self.LISTENERS: + fds = None + listen_fds = systemd.listen_fds() + if listen_fds: + self.systemd = True + fds = range(systemd.SD_LISTEN_FDS_START, + systemd.SD_LISTEN_FDS_START + listen_fds) + + elif self.master_pid: + fds = [] + for fd in os.environ.pop('GUNICORN_FD').split(','): + fds.append(int(fd)) + + self.LISTENERS = sock.create_sockets(self.cfg, self.log, fds) + + listeners_str = ",".join([str(lnr) for lnr in self.LISTENERS]) + self.log.debug("Arbiter booted") + self.log.info("Listening at: %s (%s)", listeners_str, self.pid) + self.log.info("Using worker: %s", self.cfg.worker_class_str) + systemd.sd_notify("READY=1\nSTATUS=Gunicorn arbiter booted", self.log) + + # check worker class requirements + if hasattr(self.worker_class, "check_config"): + self.worker_class.check_config(self.cfg, self.log) + + self.cfg.when_ready(self) + + def init_signals(self): + """\ + Initialize master signal handling. Most of the signals + are queued. Child signals only wake up the master. + """ + # close old PIPE + for p in self.PIPE: + os.close(p) + + # initialize the pipe + self.PIPE = pair = os.pipe() + for p in pair: + util.set_non_blocking(p) + util.close_on_exec(p) + + self.log.close_on_exec() + + # initialize all signals + for s in self.SIGNALS: + signal.signal(s, self.signal) + signal.signal(signal.SIGCHLD, self.handle_chld) + + def signal(self, sig, frame): + if len(self.SIG_QUEUE) < 5: + self.SIG_QUEUE.append(sig) + self.wakeup() + + def run(self): + "Main master loop." + self.start() + util._setproctitle("master [%s]" % self.proc_name) + + try: + self.manage_workers() + + while True: + self.maybe_promote_master() + + sig = self.SIG_QUEUE.pop(0) if self.SIG_QUEUE else None + if sig is None: + self.sleep() + self.murder_workers() + self.manage_workers() + continue + + if sig not in self.SIG_NAMES: + self.log.info("Ignoring unknown signal: %s", sig) + continue + + signame = self.SIG_NAMES.get(sig) + handler = getattr(self, "handle_%s" % signame, None) + if not handler: + self.log.error("Unhandled signal: %s", signame) + continue + self.log.info("Handling signal: %s", signame) + handler() + self.wakeup() + except (StopIteration, KeyboardInterrupt): + self.halt() + except HaltServer as inst: + self.halt(reason=inst.reason, exit_status=inst.exit_status) + except SystemExit: + raise + except Exception: + self.log.error("Unhandled exception in main loop", + exc_info=True) + self.stop(False) + if self.pidfile is not None: + self.pidfile.unlink() + sys.exit(-1) + + def handle_chld(self, sig, frame): + "SIGCHLD handling" + self.reap_workers() + self.wakeup() + + def handle_hup(self): + """\ + HUP handling. + - Reload configuration + - Start the new worker processes with a new configuration + - Gracefully shutdown the old worker processes + """ + self.log.info("Hang up: %s", self.master_name) + self.reload() + + def handle_term(self): + "SIGTERM handling" + raise StopIteration + + def handle_int(self): + "SIGINT handling" + self.stop(False) + raise StopIteration + + def handle_quit(self): + "SIGQUIT handling" + self.stop(False) + raise StopIteration + + def handle_ttin(self): + """\ + SIGTTIN handling. + Increases the number of workers by one. + """ + self.num_workers += 1 + self.manage_workers() + + def handle_ttou(self): + """\ + SIGTTOU handling. + Decreases the number of workers by one. + """ + if self.num_workers <= 1: + return + self.num_workers -= 1 + self.manage_workers() + + def handle_usr1(self): + """\ + SIGUSR1 handling. + Kill all workers by sending them a SIGUSR1 + """ + self.log.reopen_files() + self.kill_workers(signal.SIGUSR1) + + def handle_usr2(self): + """\ + SIGUSR2 handling. + Creates a new arbiter/worker set as a fork of the current + arbiter without affecting old workers. Use this to do live + deployment with the ability to backout a change. + """ + self.reexec() + + def handle_winch(self): + """SIGWINCH handling""" + if self.cfg.daemon: + self.log.info("graceful stop of workers") + self.num_workers = 0 + self.kill_workers(signal.SIGTERM) + else: + self.log.debug("SIGWINCH ignored. Not daemonized") + + def maybe_promote_master(self): + if self.master_pid == 0: + return + + if self.master_pid != os.getppid(): + self.log.info("Master has been promoted.") + # reset master infos + self.master_name = "Master" + self.master_pid = 0 + self.proc_name = self.cfg.proc_name + del os.environ['GUNICORN_PID'] + # rename the pidfile + if self.pidfile is not None: + self.pidfile.rename(self.cfg.pidfile) + # reset proctitle + util._setproctitle("master [%s]" % self.proc_name) + + def wakeup(self): + """\ + Wake up the arbiter by writing to the PIPE + """ + try: + os.write(self.PIPE[1], b'.') + except OSError as e: + if e.errno not in [errno.EAGAIN, errno.EINTR]: + raise + + def halt(self, reason=None, exit_status=0): + """ halt arbiter """ + self.stop() + + log_func = self.log.info if exit_status == 0 else self.log.error + log_func("Shutting down: %s", self.master_name) + if reason is not None: + log_func("Reason: %s", reason) + + if self.pidfile is not None: + self.pidfile.unlink() + self.cfg.on_exit(self) + sys.exit(exit_status) + + def sleep(self): + """\ + Sleep until PIPE is readable or we timeout. + A readable PIPE means a signal occurred. + """ + try: + ready = select.select([self.PIPE[0]], [], [], 1.0) + if not ready[0]: + return + while os.read(self.PIPE[0], 1): + pass + except OSError as e: + # TODO: select.error is a subclass of OSError since Python 3.3. + error_number = getattr(e, 'errno', e.args[0]) + if error_number not in [errno.EAGAIN, errno.EINTR]: + raise + except KeyboardInterrupt: + sys.exit() + + def stop(self, graceful=True): + """\ + Stop workers + + :attr graceful: boolean, If True (the default) workers will be + killed gracefully (ie. trying to wait for the current connection) + """ + unlink = ( + self.reexec_pid == self.master_pid == 0 + and not self.systemd + and not self.cfg.reuse_port + ) + sock.close_sockets(self.LISTENERS, unlink) + + self.LISTENERS = [] + sig = signal.SIGTERM + if not graceful: + sig = signal.SIGQUIT + limit = time.time() + self.cfg.graceful_timeout + # instruct the workers to exit + self.kill_workers(sig) + # wait until the graceful timeout + while self.WORKERS and time.time() < limit: + time.sleep(0.1) + + self.kill_workers(signal.SIGKILL) + + def reexec(self): + """\ + Relaunch the master and workers. + """ + if self.reexec_pid != 0: + self.log.warning("USR2 signal ignored. Child exists.") + return + + if self.master_pid != 0: + self.log.warning("USR2 signal ignored. Parent exists.") + return + + master_pid = os.getpid() + self.reexec_pid = os.fork() + if self.reexec_pid != 0: + return + + self.cfg.pre_exec(self) + + environ = self.cfg.env_orig.copy() + environ['GUNICORN_PID'] = str(master_pid) + + if self.systemd: + environ['LISTEN_PID'] = str(os.getpid()) + environ['LISTEN_FDS'] = str(len(self.LISTENERS)) + else: + environ['GUNICORN_FD'] = ','.join( + str(lnr.fileno()) for lnr in self.LISTENERS) + + os.chdir(self.START_CTX['cwd']) + + # exec the process using the original environment + os.execvpe(self.START_CTX[0], self.START_CTX['args'], environ) + + def reload(self): + old_address = self.cfg.address + + # reset old environment + for k in self.cfg.env: + if k in self.cfg.env_orig: + # reset the key to the value it had before + # we launched gunicorn + os.environ[k] = self.cfg.env_orig[k] + else: + # delete the value set by gunicorn + try: + del os.environ[k] + except KeyError: + pass + + # reload conf + self.app.reload() + self.setup(self.app) + + # reopen log files + self.log.reopen_files() + + # do we need to change listener ? + if old_address != self.cfg.address: + # close all listeners + for lnr in self.LISTENERS: + lnr.close() + # init new listeners + self.LISTENERS = sock.create_sockets(self.cfg, self.log) + listeners_str = ",".join([str(lnr) for lnr in self.LISTENERS]) + self.log.info("Listening at: %s", listeners_str) + + # do some actions on reload + self.cfg.on_reload(self) + + # unlink pidfile + if self.pidfile is not None: + self.pidfile.unlink() + + # create new pidfile + if self.cfg.pidfile is not None: + self.pidfile = Pidfile(self.cfg.pidfile) + self.pidfile.create(self.pid) + + # set new proc_name + util._setproctitle("master [%s]" % self.proc_name) + + # spawn new workers + for _ in range(self.cfg.workers): + self.spawn_worker() + + # manage workers + self.manage_workers() + + def murder_workers(self): + """\ + Kill unused/idle workers + """ + if not self.timeout: + return + workers = list(self.WORKERS.items()) + for (pid, worker) in workers: + try: + if time.monotonic() - worker.tmp.last_update() <= self.timeout: + continue + except (OSError, ValueError): + continue + + if not worker.aborted: + self.log.critical("WORKER TIMEOUT (pid:%s)", pid) + worker.aborted = True + self.kill_worker(pid, signal.SIGABRT) + else: + self.kill_worker(pid, signal.SIGKILL) + + def reap_workers(self): + """\ + Reap workers to avoid zombie processes + """ + try: + while True: + wpid, status = os.waitpid(-1, os.WNOHANG) + if not wpid: + break + if self.reexec_pid == wpid: + self.reexec_pid = 0 + else: + # A worker was terminated. If the termination reason was + # that it could not boot, we'll shut it down to avoid + # infinite start/stop cycles. + exitcode = status >> 8 + if exitcode != 0: + self.log.error('Worker (pid:%s) exited with code %s', wpid, exitcode) + if exitcode == self.WORKER_BOOT_ERROR: + reason = "Worker failed to boot." + raise HaltServer(reason, self.WORKER_BOOT_ERROR) + if exitcode == self.APP_LOAD_ERROR: + reason = "App failed to load." + raise HaltServer(reason, self.APP_LOAD_ERROR) + + if exitcode > 0: + # If the exit code of the worker is greater than 0, + # let the user know. + self.log.error("Worker (pid:%s) exited with code %s.", + wpid, exitcode) + elif status > 0: + # If the exit code of the worker is 0 and the status + # is greater than 0, then it was most likely killed + # via a signal. + try: + sig_name = signal.Signals(status).name + except ValueError: + sig_name = "code {}".format(status) + msg = "Worker (pid:{}) was sent {}!".format( + wpid, sig_name) + + # Additional hint for SIGKILL + if status == signal.SIGKILL: + msg += " Perhaps out of memory?" + self.log.error(msg) + + worker = self.WORKERS.pop(wpid, None) + if not worker: + continue + worker.tmp.close() + self.cfg.child_exit(self, worker) + except OSError as e: + if e.errno != errno.ECHILD: + raise + + def manage_workers(self): + """\ + Maintain the number of workers by spawning or killing + as required. + """ + if len(self.WORKERS) < self.num_workers: + self.spawn_workers() + + workers = self.WORKERS.items() + workers = sorted(workers, key=lambda w: w[1].age) + while len(workers) > self.num_workers: + (pid, _) = workers.pop(0) + self.kill_worker(pid, signal.SIGTERM) + + active_worker_count = len(workers) + if self._last_logged_active_worker_count != active_worker_count: + self._last_logged_active_worker_count = active_worker_count + self.log.debug("{0} workers".format(active_worker_count), + extra={"metric": "gunicorn.workers", + "value": active_worker_count, + "mtype": "gauge"}) + + def spawn_worker(self): + self.worker_age += 1 + worker = self.worker_class(self.worker_age, self.pid, self.LISTENERS, + self.app, self.timeout / 2.0, + self.cfg, self.log) + self.cfg.pre_fork(self, worker) + pid = os.fork() + if pid != 0: + worker.pid = pid + self.WORKERS[pid] = worker + return pid + + # Do not inherit the temporary files of other workers + for sibling in self.WORKERS.values(): + sibling.tmp.close() + + # Process Child + worker.pid = os.getpid() + try: + util._setproctitle("worker [%s]" % self.proc_name) + self.log.info("Booting worker with pid: %s", worker.pid) + self.cfg.post_fork(self, worker) + worker.init_process() + sys.exit(0) + except SystemExit: + raise + except AppImportError as e: + self.log.debug("Exception while loading the application", + exc_info=True) + print("%s" % e, file=sys.stderr) + sys.stderr.flush() + sys.exit(self.APP_LOAD_ERROR) + except Exception: + self.log.exception("Exception in worker process") + if not worker.booted: + sys.exit(self.WORKER_BOOT_ERROR) + sys.exit(-1) + finally: + self.log.info("Worker exiting (pid: %s)", worker.pid) + try: + worker.tmp.close() + self.cfg.worker_exit(self, worker) + except Exception: + self.log.warning("Exception during worker exit:\n%s", + traceback.format_exc()) + + def spawn_workers(self): + """\ + Spawn new workers as needed. + + This is where a worker process leaves the main loop + of the master process. + """ + + for _ in range(self.num_workers - len(self.WORKERS)): + self.spawn_worker() + time.sleep(0.1 * random.random()) + + def kill_workers(self, sig): + """\ + Kill all workers with the signal `sig` + :attr sig: `signal.SIG*` value + """ + worker_pids = list(self.WORKERS.keys()) + for pid in worker_pids: + self.kill_worker(pid, sig) + + def kill_worker(self, pid, sig): + """\ + Kill a worker + + :attr pid: int, worker pid + :attr sig: `signal.SIG*` value + """ + try: + os.kill(pid, sig) + except OSError as e: + if e.errno == errno.ESRCH: + try: + worker = self.WORKERS.pop(pid) + worker.tmp.close() + self.cfg.worker_exit(self, worker) + return + except (KeyError, OSError): + return + raise diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/config.py b/netdeploy/lib/python3.11/site-packages/gunicorn/config.py new file mode 100644 index 0000000..402a26b --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/config.py @@ -0,0 +1,2442 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +# Please remember to run "make -C docs html" after update "desc" attributes. + +import argparse +import copy +import grp +import inspect +import ipaddress +import os +import pwd +import re +import shlex +import ssl +import sys +import textwrap + +from gunicorn import __version__, util +from gunicorn.errors import ConfigError +from gunicorn.reloader import reloader_engines + +KNOWN_SETTINGS = [] +PLATFORM = sys.platform + + +def make_settings(ignore=None): + settings = {} + ignore = ignore or () + for s in KNOWN_SETTINGS: + setting = s() + if setting.name in ignore: + continue + settings[setting.name] = setting.copy() + return settings + + +def auto_int(_, x): + # for compatible with octal numbers in python3 + if re.match(r'0(\d)', x, re.IGNORECASE): + x = x.replace('0', '0o', 1) + return int(x, 0) + + +class Config: + + def __init__(self, usage=None, prog=None): + self.settings = make_settings() + self.usage = usage + self.prog = prog or os.path.basename(sys.argv[0]) + self.env_orig = os.environ.copy() + + def __str__(self): + lines = [] + kmax = max(len(k) for k in self.settings) + for k in sorted(self.settings): + v = self.settings[k].value + if callable(v): + v = "<{}()>".format(v.__qualname__) + lines.append("{k:{kmax}} = {v}".format(k=k, v=v, kmax=kmax)) + return "\n".join(lines) + + def __getattr__(self, name): + if name not in self.settings: + raise AttributeError("No configuration setting for: %s" % name) + return self.settings[name].get() + + def __setattr__(self, name, value): + if name != "settings" and name in self.settings: + raise AttributeError("Invalid access!") + super().__setattr__(name, value) + + def set(self, name, value): + if name not in self.settings: + raise AttributeError("No configuration setting for: %s" % name) + self.settings[name].set(value) + + def get_cmd_args_from_env(self): + if 'GUNICORN_CMD_ARGS' in self.env_orig: + return shlex.split(self.env_orig['GUNICORN_CMD_ARGS']) + return [] + + def parser(self): + kwargs = { + "usage": self.usage, + "prog": self.prog + } + parser = argparse.ArgumentParser(**kwargs) + parser.add_argument("-v", "--version", + action="version", default=argparse.SUPPRESS, + version="%(prog)s (version " + __version__ + ")\n", + help="show program's version number and exit") + parser.add_argument("args", nargs="*", help=argparse.SUPPRESS) + + keys = sorted(self.settings, key=self.settings.__getitem__) + for k in keys: + self.settings[k].add_option(parser) + + return parser + + @property + def worker_class_str(self): + uri = self.settings['worker_class'].get() + + if isinstance(uri, str): + # are we using a threaded worker? + is_sync = uri.endswith('SyncWorker') or uri == 'sync' + if is_sync and self.threads > 1: + return "gthread" + return uri + return uri.__name__ + + @property + def worker_class(self): + uri = self.settings['worker_class'].get() + + # are we using a threaded worker? + is_sync = isinstance(uri, str) and (uri.endswith('SyncWorker') or uri == 'sync') + if is_sync and self.threads > 1: + uri = "gunicorn.workers.gthread.ThreadWorker" + + worker_class = util.load_class(uri) + if hasattr(worker_class, "setup"): + worker_class.setup() + return worker_class + + @property + def address(self): + s = self.settings['bind'].get() + return [util.parse_address(util.bytes_to_str(bind)) for bind in s] + + @property + def uid(self): + return self.settings['user'].get() + + @property + def gid(self): + return self.settings['group'].get() + + @property + def proc_name(self): + pn = self.settings['proc_name'].get() + if pn is not None: + return pn + else: + return self.settings['default_proc_name'].get() + + @property + def logger_class(self): + uri = self.settings['logger_class'].get() + if uri == "simple": + # support the default + uri = LoggerClass.default + + # if default logger is in use, and statsd is on, automagically switch + # to the statsd logger + if uri == LoggerClass.default: + if 'statsd_host' in self.settings and self.settings['statsd_host'].value is not None: + uri = "gunicorn.instrument.statsd.Statsd" + + logger_class = util.load_class( + uri, + default="gunicorn.glogging.Logger", + section="gunicorn.loggers") + + if hasattr(logger_class, "install"): + logger_class.install() + return logger_class + + @property + def is_ssl(self): + return self.certfile or self.keyfile + + @property + def ssl_options(self): + opts = {} + for name, value in self.settings.items(): + if value.section == 'SSL': + opts[name] = value.get() + return opts + + @property + def env(self): + raw_env = self.settings['raw_env'].get() + env = {} + + if not raw_env: + return env + + for e in raw_env: + s = util.bytes_to_str(e) + try: + k, v = s.split('=', 1) + except ValueError: + raise RuntimeError("environment setting %r invalid" % s) + + env[k] = v + + return env + + @property + def sendfile(self): + if self.settings['sendfile'].get() is not None: + return False + + if 'SENDFILE' in os.environ: + sendfile = os.environ['SENDFILE'].lower() + return sendfile in ['y', '1', 'yes', 'true'] + + return True + + @property + def reuse_port(self): + return self.settings['reuse_port'].get() + + @property + def paste_global_conf(self): + raw_global_conf = self.settings['raw_paste_global_conf'].get() + if raw_global_conf is None: + return None + + global_conf = {} + for e in raw_global_conf: + s = util.bytes_to_str(e) + try: + k, v = re.split(r'(?" % ( + self.__class__.__module__, + self.__class__.__name__, + id(self), + self.value, + ) + + +Setting = SettingMeta('Setting', (Setting,), {}) + + +def validate_bool(val): + if val is None: + return + + if isinstance(val, bool): + return val + if not isinstance(val, str): + raise TypeError("Invalid type for casting: %s" % val) + if val.lower().strip() == "true": + return True + elif val.lower().strip() == "false": + return False + else: + raise ValueError("Invalid boolean: %s" % val) + + +def validate_dict(val): + if not isinstance(val, dict): + raise TypeError("Value is not a dictionary: %s " % val) + return val + + +def validate_pos_int(val): + if not isinstance(val, int): + val = int(val, 0) + else: + # Booleans are ints! + val = int(val) + if val < 0: + raise ValueError("Value must be positive: %s" % val) + return val + + +def validate_ssl_version(val): + if val != SSLVersion.default: + sys.stderr.write("Warning: option `ssl_version` is deprecated and it is ignored. Use ssl_context instead.\n") + return val + + +def validate_string(val): + if val is None: + return None + if not isinstance(val, str): + raise TypeError("Not a string: %s" % val) + return val.strip() + + +def validate_file_exists(val): + if val is None: + return None + if not os.path.exists(val): + raise ValueError("File %s does not exists." % val) + return val + + +def validate_list_string(val): + if not val: + return [] + + # legacy syntax + if isinstance(val, str): + val = [val] + + return [validate_string(v) for v in val] + + +def validate_list_of_existing_files(val): + return [validate_file_exists(v) for v in validate_list_string(val)] + + +def validate_string_to_addr_list(val): + val = validate_string_to_list(val) + + for addr in val: + if addr == "*": + continue + _vaid_ip = ipaddress.ip_address(addr) + + return val + + +def validate_string_to_list(val): + val = validate_string(val) + + if not val: + return [] + + return [v.strip() for v in val.split(",") if v] + + +def validate_class(val): + if inspect.isfunction(val) or inspect.ismethod(val): + val = val() + if inspect.isclass(val): + return val + return validate_string(val) + + +def validate_callable(arity): + def _validate_callable(val): + if isinstance(val, str): + try: + mod_name, obj_name = val.rsplit(".", 1) + except ValueError: + raise TypeError("Value '%s' is not import string. " + "Format: module[.submodules...].object" % val) + try: + mod = __import__(mod_name, fromlist=[obj_name]) + val = getattr(mod, obj_name) + except ImportError as e: + raise TypeError(str(e)) + except AttributeError: + raise TypeError("Can not load '%s' from '%s'" + "" % (obj_name, mod_name)) + if not callable(val): + raise TypeError("Value is not callable: %s" % val) + if arity != -1 and arity != util.get_arity(val): + raise TypeError("Value must have an arity of: %s" % arity) + return val + return _validate_callable + + +def validate_user(val): + if val is None: + return os.geteuid() + if isinstance(val, int): + return val + elif val.isdigit(): + return int(val) + else: + try: + return pwd.getpwnam(val).pw_uid + except KeyError: + raise ConfigError("No such user: '%s'" % val) + + +def validate_group(val): + if val is None: + return os.getegid() + + if isinstance(val, int): + return val + elif val.isdigit(): + return int(val) + else: + try: + return grp.getgrnam(val).gr_gid + except KeyError: + raise ConfigError("No such group: '%s'" % val) + + +def validate_post_request(val): + val = validate_callable(-1)(val) + + largs = util.get_arity(val) + if largs == 4: + return val + elif largs == 3: + return lambda worker, req, env, _r: val(worker, req, env) + elif largs == 2: + return lambda worker, req, _e, _r: val(worker, req) + else: + raise TypeError("Value must have an arity of: 4") + + +def validate_chdir(val): + # valid if the value is a string + val = validate_string(val) + + # transform relative paths + path = os.path.abspath(os.path.normpath(os.path.join(util.getcwd(), val))) + + # test if the path exists + if not os.path.exists(path): + raise ConfigError("can't chdir to %r" % val) + + return path + + +def validate_statsd_address(val): + val = validate_string(val) + if val is None: + return None + + # As of major release 20, util.parse_address would recognize unix:PORT + # as a UDS address, breaking backwards compatibility. We defend against + # that regression here (this is also unit-tested). + # Feel free to remove in the next major release. + unix_hostname_regression = re.match(r'^unix:(\d+)$', val) + if unix_hostname_regression: + return ('unix', int(unix_hostname_regression.group(1))) + + try: + address = util.parse_address(val, default_port='8125') + except RuntimeError: + raise TypeError("Value must be one of ('host:port', 'unix://PATH')") + + return address + + +def validate_reload_engine(val): + if val not in reloader_engines: + raise ConfigError("Invalid reload_engine: %r" % val) + + return val + + +def get_default_config_file(): + config_path = os.path.join(os.path.abspath(os.getcwd()), + 'gunicorn.conf.py') + if os.path.exists(config_path): + return config_path + return None + + +class ConfigFile(Setting): + name = "config" + section = "Config File" + cli = ["-c", "--config"] + meta = "CONFIG" + validator = validate_string + default = "./gunicorn.conf.py" + desc = """\ + :ref:`The Gunicorn config file`. + + A string of the form ``PATH``, ``file:PATH``, or ``python:MODULE_NAME``. + + Only has an effect when specified on the command line or as part of an + application specific configuration. + + By default, a file named ``gunicorn.conf.py`` will be read from the same + directory where gunicorn is being run. + + .. versionchanged:: 19.4 + Loading the config from a Python module requires the ``python:`` + prefix. + """ + + +class WSGIApp(Setting): + name = "wsgi_app" + section = "Config File" + meta = "STRING" + validator = validate_string + default = None + desc = """\ + A WSGI application path in pattern ``$(MODULE_NAME):$(VARIABLE_NAME)``. + + .. versionadded:: 20.1.0 + """ + + +class Bind(Setting): + name = "bind" + action = "append" + section = "Server Socket" + cli = ["-b", "--bind"] + meta = "ADDRESS" + validator = validate_list_string + + if 'PORT' in os.environ: + default = ['0.0.0.0:{0}'.format(os.environ.get('PORT'))] + else: + default = ['127.0.0.1:8000'] + + desc = """\ + The socket to bind. + + A string of the form: ``HOST``, ``HOST:PORT``, ``unix:PATH``, + ``fd://FD``. An IP is a valid ``HOST``. + + .. versionchanged:: 20.0 + Support for ``fd://FD`` got added. + + Multiple addresses can be bound. ex.:: + + $ gunicorn -b 127.0.0.1:8000 -b [::1]:8000 test:app + + will bind the `test:app` application on localhost both on ipv6 + and ipv4 interfaces. + + If the ``PORT`` environment variable is defined, the default + is ``['0.0.0.0:$PORT']``. If it is not defined, the default + is ``['127.0.0.1:8000']``. + """ + + +class Backlog(Setting): + name = "backlog" + section = "Server Socket" + cli = ["--backlog"] + meta = "INT" + validator = validate_pos_int + type = int + default = 2048 + desc = """\ + The maximum number of pending connections. + + This refers to the number of clients that can be waiting to be served. + Exceeding this number results in the client getting an error when + attempting to connect. It should only affect servers under significant + load. + + Must be a positive integer. Generally set in the 64-2048 range. + """ + + +class Workers(Setting): + name = "workers" + section = "Worker Processes" + cli = ["-w", "--workers"] + meta = "INT" + validator = validate_pos_int + type = int + default = int(os.environ.get("WEB_CONCURRENCY", 1)) + desc = """\ + The number of worker processes for handling requests. + + A positive integer generally in the ``2-4 x $(NUM_CORES)`` range. + You'll want to vary this a bit to find the best for your particular + application's work load. + + By default, the value of the ``WEB_CONCURRENCY`` environment variable, + which is set by some Platform-as-a-Service providers such as Heroku. If + it is not defined, the default is ``1``. + """ + + +class WorkerClass(Setting): + name = "worker_class" + section = "Worker Processes" + cli = ["-k", "--worker-class"] + meta = "STRING" + validator = validate_class + default = "sync" + desc = """\ + The type of workers to use. + + The default class (``sync``) should handle most "normal" types of + workloads. You'll want to read :doc:`design` for information on when + you might want to choose one of the other worker classes. Required + libraries may be installed using setuptools' ``extras_require`` feature. + + A string referring to one of the following bundled classes: + + * ``sync`` + * ``eventlet`` - Requires eventlet >= 0.24.1 (or install it via + ``pip install gunicorn[eventlet]``) + * ``gevent`` - Requires gevent >= 1.4 (or install it via + ``pip install gunicorn[gevent]``) + * ``tornado`` - Requires tornado >= 0.2 (or install it via + ``pip install gunicorn[tornado]``) + * ``gthread`` - Python 2 requires the futures package to be installed + (or install it via ``pip install gunicorn[gthread]``) + + Optionally, you can provide your own worker by giving Gunicorn a + Python path to a subclass of ``gunicorn.workers.base.Worker``. + This alternative syntax will load the gevent class: + ``gunicorn.workers.ggevent.GeventWorker``. + """ + + +class WorkerThreads(Setting): + name = "threads" + section = "Worker Processes" + cli = ["--threads"] + meta = "INT" + validator = validate_pos_int + type = int + default = 1 + desc = """\ + The number of worker threads for handling requests. + + Run each worker with the specified number of threads. + + A positive integer generally in the ``2-4 x $(NUM_CORES)`` range. + You'll want to vary this a bit to find the best for your particular + application's work load. + + If it is not defined, the default is ``1``. + + This setting only affects the Gthread worker type. + + .. note:: + If you try to use the ``sync`` worker type and set the ``threads`` + setting to more than 1, the ``gthread`` worker type will be used + instead. + """ + + +class WorkerConnections(Setting): + name = "worker_connections" + section = "Worker Processes" + cli = ["--worker-connections"] + meta = "INT" + validator = validate_pos_int + type = int + default = 1000 + desc = """\ + The maximum number of simultaneous clients. + + This setting only affects the ``gthread``, ``eventlet`` and ``gevent`` worker types. + """ + + +class MaxRequests(Setting): + name = "max_requests" + section = "Worker Processes" + cli = ["--max-requests"] + meta = "INT" + validator = validate_pos_int + type = int + default = 0 + desc = """\ + The maximum number of requests a worker will process before restarting. + + Any value greater than zero will limit the number of requests a worker + will process before automatically restarting. This is a simple method + to help limit the damage of memory leaks. + + If this is set to zero (the default) then the automatic worker + restarts are disabled. + """ + + +class MaxRequestsJitter(Setting): + name = "max_requests_jitter" + section = "Worker Processes" + cli = ["--max-requests-jitter"] + meta = "INT" + validator = validate_pos_int + type = int + default = 0 + desc = """\ + The maximum jitter to add to the *max_requests* setting. + + The jitter causes the restart per worker to be randomized by + ``randint(0, max_requests_jitter)``. This is intended to stagger worker + restarts to avoid all workers restarting at the same time. + + .. versionadded:: 19.2 + """ + + +class Timeout(Setting): + name = "timeout" + section = "Worker Processes" + cli = ["-t", "--timeout"] + meta = "INT" + validator = validate_pos_int + type = int + default = 30 + desc = """\ + Workers silent for more than this many seconds are killed and restarted. + + Value is a positive number or 0. Setting it to 0 has the effect of + infinite timeouts by disabling timeouts for all workers entirely. + + Generally, the default of thirty seconds should suffice. Only set this + noticeably higher if you're sure of the repercussions for sync workers. + For the non sync workers it just means that the worker process is still + communicating and is not tied to the length of time required to handle a + single request. + """ + + +class GracefulTimeout(Setting): + name = "graceful_timeout" + section = "Worker Processes" + cli = ["--graceful-timeout"] + meta = "INT" + validator = validate_pos_int + type = int + default = 30 + desc = """\ + Timeout for graceful workers restart. + + After receiving a restart signal, workers have this much time to finish + serving requests. Workers still alive after the timeout (starting from + the receipt of the restart signal) are force killed. + """ + + +class Keepalive(Setting): + name = "keepalive" + section = "Worker Processes" + cli = ["--keep-alive"] + meta = "INT" + validator = validate_pos_int + type = int + default = 2 + desc = """\ + The number of seconds to wait for requests on a Keep-Alive connection. + + Generally set in the 1-5 seconds range for servers with direct connection + to the client (e.g. when you don't have separate load balancer). When + Gunicorn is deployed behind a load balancer, it often makes sense to + set this to a higher value. + + .. note:: + ``sync`` worker does not support persistent connections and will + ignore this option. + """ + + +class LimitRequestLine(Setting): + name = "limit_request_line" + section = "Security" + cli = ["--limit-request-line"] + meta = "INT" + validator = validate_pos_int + type = int + default = 4094 + desc = """\ + The maximum size of HTTP request line in bytes. + + This parameter is used to limit the allowed size of a client's + HTTP request-line. Since the request-line consists of the HTTP + method, URI, and protocol version, this directive places a + restriction on the length of a request-URI allowed for a request + on the server. A server needs this value to be large enough to + hold any of its resource names, including any information that + might be passed in the query part of a GET request. Value is a number + from 0 (unlimited) to 8190. + + This parameter can be used to prevent any DDOS attack. + """ + + +class LimitRequestFields(Setting): + name = "limit_request_fields" + section = "Security" + cli = ["--limit-request-fields"] + meta = "INT" + validator = validate_pos_int + type = int + default = 100 + desc = """\ + Limit the number of HTTP headers fields in a request. + + This parameter is used to limit the number of headers in a request to + prevent DDOS attack. Used with the *limit_request_field_size* it allows + more safety. By default this value is 100 and can't be larger than + 32768. + """ + + +class LimitRequestFieldSize(Setting): + name = "limit_request_field_size" + section = "Security" + cli = ["--limit-request-field_size"] + meta = "INT" + validator = validate_pos_int + type = int + default = 8190 + desc = """\ + Limit the allowed size of an HTTP request header field. + + Value is a positive number or 0. Setting it to 0 will allow unlimited + header field sizes. + + .. warning:: + Setting this parameter to a very high or unlimited value can open + up for DDOS attacks. + """ + + +class Reload(Setting): + name = "reload" + section = 'Debugging' + cli = ['--reload'] + validator = validate_bool + action = 'store_true' + default = False + + desc = '''\ + Restart workers when code changes. + + This setting is intended for development. It will cause workers to be + restarted whenever application code changes. + + The reloader is incompatible with application preloading. When using a + paste configuration be sure that the server block does not import any + application code or the reload will not work as designed. + + The default behavior is to attempt inotify with a fallback to file + system polling. Generally, inotify should be preferred if available + because it consumes less system resources. + + .. note:: + In order to use the inotify reloader, you must have the ``inotify`` + package installed. + ''' + + +class ReloadEngine(Setting): + name = "reload_engine" + section = "Debugging" + cli = ["--reload-engine"] + meta = "STRING" + validator = validate_reload_engine + default = "auto" + desc = """\ + The implementation that should be used to power :ref:`reload`. + + Valid engines are: + + * ``'auto'`` + * ``'poll'`` + * ``'inotify'`` (requires inotify) + + .. versionadded:: 19.7 + """ + + +class ReloadExtraFiles(Setting): + name = "reload_extra_files" + action = "append" + section = "Debugging" + cli = ["--reload-extra-file"] + meta = "FILES" + validator = validate_list_of_existing_files + default = [] + desc = """\ + Extends :ref:`reload` option to also watch and reload on additional files + (e.g., templates, configurations, specifications, etc.). + + .. versionadded:: 19.8 + """ + + +class Spew(Setting): + name = "spew" + section = "Debugging" + cli = ["--spew"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Install a trace function that spews every line executed by the server. + + This is the nuclear option. + """ + + +class ConfigCheck(Setting): + name = "check_config" + section = "Debugging" + cli = ["--check-config"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Check the configuration and exit. The exit status is 0 if the + configuration is correct, and 1 if the configuration is incorrect. + """ + + +class PrintConfig(Setting): + name = "print_config" + section = "Debugging" + cli = ["--print-config"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Print the configuration settings as fully resolved. Implies :ref:`check-config`. + """ + + +class PreloadApp(Setting): + name = "preload_app" + section = "Server Mechanics" + cli = ["--preload"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Load application code before the worker processes are forked. + + By preloading an application you can save some RAM resources as well as + speed up server boot times. Although, if you defer application loading + to each worker process, you can reload your application code easily by + restarting workers. + """ + + +class Sendfile(Setting): + name = "sendfile" + section = "Server Mechanics" + cli = ["--no-sendfile"] + validator = validate_bool + action = "store_const" + const = False + + desc = """\ + Disables the use of ``sendfile()``. + + If not set, the value of the ``SENDFILE`` environment variable is used + to enable or disable its usage. + + .. versionadded:: 19.2 + .. versionchanged:: 19.4 + Swapped ``--sendfile`` with ``--no-sendfile`` to actually allow + disabling. + .. versionchanged:: 19.6 + added support for the ``SENDFILE`` environment variable + """ + + +class ReusePort(Setting): + name = "reuse_port" + section = "Server Mechanics" + cli = ["--reuse-port"] + validator = validate_bool + action = "store_true" + default = False + + desc = """\ + Set the ``SO_REUSEPORT`` flag on the listening socket. + + .. versionadded:: 19.8 + """ + + +class Chdir(Setting): + name = "chdir" + section = "Server Mechanics" + cli = ["--chdir"] + validator = validate_chdir + default = util.getcwd() + default_doc = "``'.'``" + desc = """\ + Change directory to specified directory before loading apps. + """ + + +class Daemon(Setting): + name = "daemon" + section = "Server Mechanics" + cli = ["-D", "--daemon"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Daemonize the Gunicorn process. + + Detaches the server from the controlling terminal and enters the + background. + """ + + +class Env(Setting): + name = "raw_env" + action = "append" + section = "Server Mechanics" + cli = ["-e", "--env"] + meta = "ENV" + validator = validate_list_string + default = [] + + desc = """\ + Set environment variables in the execution environment. + + Should be a list of strings in the ``key=value`` format. + + For example on the command line: + + .. code-block:: console + + $ gunicorn -b 127.0.0.1:8000 --env FOO=1 test:app + + Or in the configuration file: + + .. code-block:: python + + raw_env = ["FOO=1"] + """ + + +class Pidfile(Setting): + name = "pidfile" + section = "Server Mechanics" + cli = ["-p", "--pid"] + meta = "FILE" + validator = validate_string + default = None + desc = """\ + A filename to use for the PID file. + + If not set, no PID file will be written. + """ + + +class WorkerTmpDir(Setting): + name = "worker_tmp_dir" + section = "Server Mechanics" + cli = ["--worker-tmp-dir"] + meta = "DIR" + validator = validate_string + default = None + desc = """\ + A directory to use for the worker heartbeat temporary file. + + If not set, the default temporary directory will be used. + + .. note:: + The current heartbeat system involves calling ``os.fchmod`` on + temporary file handlers and may block a worker for arbitrary time + if the directory is on a disk-backed filesystem. + + See :ref:`blocking-os-fchmod` for more detailed information + and a solution for avoiding this problem. + """ + + +class User(Setting): + name = "user" + section = "Server Mechanics" + cli = ["-u", "--user"] + meta = "USER" + validator = validate_user + default = os.geteuid() + default_doc = "``os.geteuid()``" + desc = """\ + Switch worker processes to run as this user. + + A valid user id (as an integer) or the name of a user that can be + retrieved with a call to ``pwd.getpwnam(value)`` or ``None`` to not + change the worker process user. + """ + + +class Group(Setting): + name = "group" + section = "Server Mechanics" + cli = ["-g", "--group"] + meta = "GROUP" + validator = validate_group + default = os.getegid() + default_doc = "``os.getegid()``" + desc = """\ + Switch worker process to run as this group. + + A valid group id (as an integer) or the name of a user that can be + retrieved with a call to ``pwd.getgrnam(value)`` or ``None`` to not + change the worker processes group. + """ + + +class Umask(Setting): + name = "umask" + section = "Server Mechanics" + cli = ["-m", "--umask"] + meta = "INT" + validator = validate_pos_int + type = auto_int + default = 0 + desc = """\ + A bit mask for the file mode on files written by Gunicorn. + + Note that this affects unix socket permissions. + + A valid value for the ``os.umask(mode)`` call or a string compatible + with ``int(value, 0)`` (``0`` means Python guesses the base, so values + like ``0``, ``0xFF``, ``0022`` are valid for decimal, hex, and octal + representations) + """ + + +class Initgroups(Setting): + name = "initgroups" + section = "Server Mechanics" + cli = ["--initgroups"] + validator = validate_bool + action = 'store_true' + default = False + + desc = """\ + If true, set the worker process's group access list with all of the + groups of which the specified username is a member, plus the specified + group id. + + .. versionadded:: 19.7 + """ + + +class TmpUploadDir(Setting): + name = "tmp_upload_dir" + section = "Server Mechanics" + meta = "DIR" + validator = validate_string + default = None + desc = """\ + Directory to store temporary request data as they are read. + + This may disappear in the near future. + + This path should be writable by the process permissions set for Gunicorn + workers. If not specified, Gunicorn will choose a system generated + temporary directory. + """ + + +class SecureSchemeHeader(Setting): + name = "secure_scheme_headers" + section = "Server Mechanics" + validator = validate_dict + default = { + "X-FORWARDED-PROTOCOL": "ssl", + "X-FORWARDED-PROTO": "https", + "X-FORWARDED-SSL": "on" + } + desc = """\ + + A dictionary containing headers and values that the front-end proxy + uses to indicate HTTPS requests. If the source IP is permitted by + :ref:`forwarded-allow-ips` (below), *and* at least one request header matches + a key-value pair listed in this dictionary, then Gunicorn will set + ``wsgi.url_scheme`` to ``https``, so your application can tell that the + request is secure. + + If the other headers listed in this dictionary are not present in the request, they will be ignored, + but if the other headers are present and do not match the provided values, then + the request will fail to parse. See the note below for more detailed examples of this behaviour. + + The dictionary should map upper-case header names to exact string + values. The value comparisons are case-sensitive, unlike the header + names, so make sure they're exactly what your front-end proxy sends + when handling HTTPS requests. + + It is important that your front-end proxy configuration ensures that + the headers defined here can not be passed directly from the client. + """ + + +class ForwardedAllowIPS(Setting): + name = "forwarded_allow_ips" + section = "Server Mechanics" + cli = ["--forwarded-allow-ips"] + meta = "STRING" + validator = validate_string_to_addr_list + default = os.environ.get("FORWARDED_ALLOW_IPS", "127.0.0.1,::1") + desc = """\ + Front-end's IPs from which allowed to handle set secure headers. + (comma separated). + + Set to ``*`` to disable checking of front-end IPs. This is useful for setups + where you don't know in advance the IP address of front-end, but + instead have ensured via other means that only your + authorized front-ends can access Gunicorn. + + By default, the value of the ``FORWARDED_ALLOW_IPS`` environment + variable. If it is not defined, the default is ``"127.0.0.1,::1"``. + + .. note:: + + This option does not affect UNIX socket connections. Connections not associated with + an IP address are treated as allowed, unconditionally. + + .. note:: + + The interplay between the request headers, the value of ``forwarded_allow_ips``, and the value of + ``secure_scheme_headers`` is complex. Various scenarios are documented below to further elaborate. + In each case, we have a request from the remote address 134.213.44.18, and the default value of + ``secure_scheme_headers``: + + .. code:: + + secure_scheme_headers = { + 'X-FORWARDED-PROTOCOL': 'ssl', + 'X-FORWARDED-PROTO': 'https', + 'X-FORWARDED-SSL': 'on' + } + + + .. list-table:: + :header-rows: 1 + :align: center + :widths: auto + + * - ``forwarded-allow-ips`` + - Secure Request Headers + - Result + - Explanation + * - .. code:: + + ["127.0.0.1"] + - .. code:: + + X-Forwarded-Proto: https + - .. code:: + + wsgi.url_scheme = "http" + - IP address was not allowed + * - .. code:: + + "*" + - + - .. code:: + + wsgi.url_scheme = "http" + - IP address allowed, but no secure headers provided + * - .. code:: + + "*" + - .. code:: + + X-Forwarded-Proto: https + - .. code:: + + wsgi.url_scheme = "https" + - IP address allowed, one request header matched + * - .. code:: + + ["134.213.44.18"] + - .. code:: + + X-Forwarded-Ssl: on + X-Forwarded-Proto: http + - ``InvalidSchemeHeaders()`` raised + - IP address allowed, but the two secure headers disagreed on if HTTPS was used + + + """ + + +class AccessLog(Setting): + name = "accesslog" + section = "Logging" + cli = ["--access-logfile"] + meta = "FILE" + validator = validate_string + default = None + desc = """\ + The Access log file to write to. + + ``'-'`` means log to stdout. + """ + + +class DisableRedirectAccessToSyslog(Setting): + name = "disable_redirect_access_to_syslog" + section = "Logging" + cli = ["--disable-redirect-access-to-syslog"] + validator = validate_bool + action = 'store_true' + default = False + desc = """\ + Disable redirect access logs to syslog. + + .. versionadded:: 19.8 + """ + + +class AccessLogFormat(Setting): + name = "access_log_format" + section = "Logging" + cli = ["--access-logformat"] + meta = "STRING" + validator = validate_string + default = '%(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"' + desc = """\ + The access log format. + + =========== =========== + Identifier Description + =========== =========== + h remote address + l ``'-'`` + u user name (if HTTP Basic auth used) + t date of the request + r status line (e.g. ``GET / HTTP/1.1``) + m request method + U URL path without query string + q query string + H protocol + s status + B response length + b response length or ``'-'`` (CLF format) + f referrer (note: header is ``referer``) + a user agent + T request time in seconds + M request time in milliseconds + D request time in microseconds + L request time in decimal seconds + p process ID + {header}i request header + {header}o response header + {variable}e environment variable + =========== =========== + + Use lowercase for header and environment variable names, and put + ``{...}x`` names inside ``%(...)s``. For example:: + + %({x-forwarded-for}i)s + """ + + +class ErrorLog(Setting): + name = "errorlog" + section = "Logging" + cli = ["--error-logfile", "--log-file"] + meta = "FILE" + validator = validate_string + default = '-' + desc = """\ + The Error log file to write to. + + Using ``'-'`` for FILE makes gunicorn log to stderr. + + .. versionchanged:: 19.2 + Log to stderr by default. + + """ + + +class Loglevel(Setting): + name = "loglevel" + section = "Logging" + cli = ["--log-level"] + meta = "LEVEL" + validator = validate_string + default = "info" + desc = """\ + The granularity of Error log outputs. + + Valid level names are: + + * ``'debug'`` + * ``'info'`` + * ``'warning'`` + * ``'error'`` + * ``'critical'`` + """ + + +class CaptureOutput(Setting): + name = "capture_output" + section = "Logging" + cli = ["--capture-output"] + validator = validate_bool + action = 'store_true' + default = False + desc = """\ + Redirect stdout/stderr to specified file in :ref:`errorlog`. + + .. versionadded:: 19.6 + """ + + +class LoggerClass(Setting): + name = "logger_class" + section = "Logging" + cli = ["--logger-class"] + meta = "STRING" + validator = validate_class + default = "gunicorn.glogging.Logger" + desc = """\ + The logger you want to use to log events in Gunicorn. + + The default class (``gunicorn.glogging.Logger``) handles most + normal usages in logging. It provides error and access logging. + + You can provide your own logger by giving Gunicorn a Python path to a + class that quacks like ``gunicorn.glogging.Logger``. + """ + + +class LogConfig(Setting): + name = "logconfig" + section = "Logging" + cli = ["--log-config"] + meta = "FILE" + validator = validate_string + default = None + desc = """\ + The log config file to use. + Gunicorn uses the standard Python logging module's Configuration + file format. + """ + + +class LogConfigDict(Setting): + name = "logconfig_dict" + section = "Logging" + validator = validate_dict + default = {} + desc = """\ + The log config dictionary to use, using the standard Python + logging module's dictionary configuration format. This option + takes precedence over the :ref:`logconfig` and :ref:`logconfig-json` options, + which uses the older file configuration format and JSON + respectively. + + Format: https://docs.python.org/3/library/logging.config.html#logging.config.dictConfig + + For more context you can look at the default configuration dictionary for logging, + which can be found at ``gunicorn.glogging.CONFIG_DEFAULTS``. + + .. versionadded:: 19.8 + """ + + +class LogConfigJson(Setting): + name = "logconfig_json" + section = "Logging" + cli = ["--log-config-json"] + meta = "FILE" + validator = validate_string + default = None + desc = """\ + The log config to read config from a JSON file + + Format: https://docs.python.org/3/library/logging.config.html#logging.config.jsonConfig + + .. versionadded:: 20.0 + """ + + +class SyslogTo(Setting): + name = "syslog_addr" + section = "Logging" + cli = ["--log-syslog-to"] + meta = "SYSLOG_ADDR" + validator = validate_string + + if PLATFORM == "darwin": + default = "unix:///var/run/syslog" + elif PLATFORM in ('freebsd', 'dragonfly', ): + default = "unix:///var/run/log" + elif PLATFORM == "openbsd": + default = "unix:///dev/log" + else: + default = "udp://localhost:514" + + desc = """\ + Address to send syslog messages. + + Address is a string of the form: + + * ``unix://PATH#TYPE`` : for unix domain socket. ``TYPE`` can be ``stream`` + for the stream driver or ``dgram`` for the dgram driver. + ``stream`` is the default. + * ``udp://HOST:PORT`` : for UDP sockets + * ``tcp://HOST:PORT`` : for TCP sockets + + """ + + +class Syslog(Setting): + name = "syslog" + section = "Logging" + cli = ["--log-syslog"] + validator = validate_bool + action = 'store_true' + default = False + desc = """\ + Send *Gunicorn* logs to syslog. + + .. versionchanged:: 19.8 + You can now disable sending access logs by using the + :ref:`disable-redirect-access-to-syslog` setting. + """ + + +class SyslogPrefix(Setting): + name = "syslog_prefix" + section = "Logging" + cli = ["--log-syslog-prefix"] + meta = "SYSLOG_PREFIX" + validator = validate_string + default = None + desc = """\ + Makes Gunicorn use the parameter as program-name in the syslog entries. + + All entries will be prefixed by ``gunicorn.``. By default the + program name is the name of the process. + """ + + +class SyslogFacility(Setting): + name = "syslog_facility" + section = "Logging" + cli = ["--log-syslog-facility"] + meta = "SYSLOG_FACILITY" + validator = validate_string + default = "user" + desc = """\ + Syslog facility name + """ + + +class EnableStdioInheritance(Setting): + name = "enable_stdio_inheritance" + section = "Logging" + cli = ["-R", "--enable-stdio-inheritance"] + validator = validate_bool + default = False + action = "store_true" + desc = """\ + Enable stdio inheritance. + + Enable inheritance for stdio file descriptors in daemon mode. + + Note: To disable the Python stdout buffering, you can to set the user + environment variable ``PYTHONUNBUFFERED`` . + """ + + +# statsD monitoring +class StatsdHost(Setting): + name = "statsd_host" + section = "Logging" + cli = ["--statsd-host"] + meta = "STATSD_ADDR" + default = None + validator = validate_statsd_address + desc = """\ + The address of the StatsD server to log to. + + Address is a string of the form: + + * ``unix://PATH`` : for a unix domain socket. + * ``HOST:PORT`` : for a network address + + .. versionadded:: 19.1 + """ + + +# Datadog Statsd (dogstatsd) tags. https://docs.datadoghq.com/developers/dogstatsd/ +class DogstatsdTags(Setting): + name = "dogstatsd_tags" + section = "Logging" + cli = ["--dogstatsd-tags"] + meta = "DOGSTATSD_TAGS" + default = "" + validator = validate_string + desc = """\ + A comma-delimited list of datadog statsd (dogstatsd) tags to append to + statsd metrics. + + .. versionadded:: 20 + """ + + +class StatsdPrefix(Setting): + name = "statsd_prefix" + section = "Logging" + cli = ["--statsd-prefix"] + meta = "STATSD_PREFIX" + default = "" + validator = validate_string + desc = """\ + Prefix to use when emitting statsd metrics (a trailing ``.`` is added, + if not provided). + + .. versionadded:: 19.2 + """ + + +class Procname(Setting): + name = "proc_name" + section = "Process Naming" + cli = ["-n", "--name"] + meta = "STRING" + validator = validate_string + default = None + desc = """\ + A base to use with setproctitle for process naming. + + This affects things like ``ps`` and ``top``. If you're going to be + running more than one instance of Gunicorn you'll probably want to set a + name to tell them apart. This requires that you install the setproctitle + module. + + If not set, the *default_proc_name* setting will be used. + """ + + +class DefaultProcName(Setting): + name = "default_proc_name" + section = "Process Naming" + validator = validate_string + default = "gunicorn" + desc = """\ + Internal setting that is adjusted for each type of application. + """ + + +class PythonPath(Setting): + name = "pythonpath" + section = "Server Mechanics" + cli = ["--pythonpath"] + meta = "STRING" + validator = validate_string + default = None + desc = """\ + A comma-separated list of directories to add to the Python path. + + e.g. + ``'/home/djangoprojects/myproject,/home/python/mylibrary'``. + """ + + +class Paste(Setting): + name = "paste" + section = "Server Mechanics" + cli = ["--paste", "--paster"] + meta = "STRING" + validator = validate_string + default = None + desc = """\ + Load a PasteDeploy config file. The argument may contain a ``#`` + symbol followed by the name of an app section from the config file, + e.g. ``production.ini#admin``. + + At this time, using alternate server blocks is not supported. Use the + command line arguments to control server configuration instead. + """ + + +class OnStarting(Setting): + name = "on_starting" + section = "Server Hooks" + validator = validate_callable(1) + type = callable + + def on_starting(server): + pass + default = staticmethod(on_starting) + desc = """\ + Called just before the master process is initialized. + + The callable needs to accept a single instance variable for the Arbiter. + """ + + +class OnReload(Setting): + name = "on_reload" + section = "Server Hooks" + validator = validate_callable(1) + type = callable + + def on_reload(server): + pass + default = staticmethod(on_reload) + desc = """\ + Called to recycle workers during a reload via SIGHUP. + + The callable needs to accept a single instance variable for the Arbiter. + """ + + +class WhenReady(Setting): + name = "when_ready" + section = "Server Hooks" + validator = validate_callable(1) + type = callable + + def when_ready(server): + pass + default = staticmethod(when_ready) + desc = """\ + Called just after the server is started. + + The callable needs to accept a single instance variable for the Arbiter. + """ + + +class Prefork(Setting): + name = "pre_fork" + section = "Server Hooks" + validator = validate_callable(2) + type = callable + + def pre_fork(server, worker): + pass + default = staticmethod(pre_fork) + desc = """\ + Called just before a worker is forked. + + The callable needs to accept two instance variables for the Arbiter and + new Worker. + """ + + +class Postfork(Setting): + name = "post_fork" + section = "Server Hooks" + validator = validate_callable(2) + type = callable + + def post_fork(server, worker): + pass + default = staticmethod(post_fork) + desc = """\ + Called just after a worker has been forked. + + The callable needs to accept two instance variables for the Arbiter and + new Worker. + """ + + +class PostWorkerInit(Setting): + name = "post_worker_init" + section = "Server Hooks" + validator = validate_callable(1) + type = callable + + def post_worker_init(worker): + pass + + default = staticmethod(post_worker_init) + desc = """\ + Called just after a worker has initialized the application. + + The callable needs to accept one instance variable for the initialized + Worker. + """ + + +class WorkerInt(Setting): + name = "worker_int" + section = "Server Hooks" + validator = validate_callable(1) + type = callable + + def worker_int(worker): + pass + + default = staticmethod(worker_int) + desc = """\ + Called just after a worker exited on SIGINT or SIGQUIT. + + The callable needs to accept one instance variable for the initialized + Worker. + """ + + +class WorkerAbort(Setting): + name = "worker_abort" + section = "Server Hooks" + validator = validate_callable(1) + type = callable + + def worker_abort(worker): + pass + + default = staticmethod(worker_abort) + desc = """\ + Called when a worker received the SIGABRT signal. + + This call generally happens on timeout. + + The callable needs to accept one instance variable for the initialized + Worker. + """ + + +class PreExec(Setting): + name = "pre_exec" + section = "Server Hooks" + validator = validate_callable(1) + type = callable + + def pre_exec(server): + pass + default = staticmethod(pre_exec) + desc = """\ + Called just before a new master process is forked. + + The callable needs to accept a single instance variable for the Arbiter. + """ + + +class PreRequest(Setting): + name = "pre_request" + section = "Server Hooks" + validator = validate_callable(2) + type = callable + + def pre_request(worker, req): + worker.log.debug("%s %s", req.method, req.path) + default = staticmethod(pre_request) + desc = """\ + Called just before a worker processes the request. + + The callable needs to accept two instance variables for the Worker and + the Request. + """ + + +class PostRequest(Setting): + name = "post_request" + section = "Server Hooks" + validator = validate_post_request + type = callable + + def post_request(worker, req, environ, resp): + pass + default = staticmethod(post_request) + desc = """\ + Called after a worker processes the request. + + The callable needs to accept two instance variables for the Worker and + the Request. + """ + + +class ChildExit(Setting): + name = "child_exit" + section = "Server Hooks" + validator = validate_callable(2) + type = callable + + def child_exit(server, worker): + pass + default = staticmethod(child_exit) + desc = """\ + Called just after a worker has been exited, in the master process. + + The callable needs to accept two instance variables for the Arbiter and + the just-exited Worker. + + .. versionadded:: 19.7 + """ + + +class WorkerExit(Setting): + name = "worker_exit" + section = "Server Hooks" + validator = validate_callable(2) + type = callable + + def worker_exit(server, worker): + pass + default = staticmethod(worker_exit) + desc = """\ + Called just after a worker has been exited, in the worker process. + + The callable needs to accept two instance variables for the Arbiter and + the just-exited Worker. + """ + + +class NumWorkersChanged(Setting): + name = "nworkers_changed" + section = "Server Hooks" + validator = validate_callable(3) + type = callable + + def nworkers_changed(server, new_value, old_value): + pass + default = staticmethod(nworkers_changed) + desc = """\ + Called just after *num_workers* has been changed. + + The callable needs to accept an instance variable of the Arbiter and + two integers of number of workers after and before change. + + If the number of workers is set for the first time, *old_value* would + be ``None``. + """ + + +class OnExit(Setting): + name = "on_exit" + section = "Server Hooks" + validator = validate_callable(1) + + def on_exit(server): + pass + + default = staticmethod(on_exit) + desc = """\ + Called just before exiting Gunicorn. + + The callable needs to accept a single instance variable for the Arbiter. + """ + + +class NewSSLContext(Setting): + name = "ssl_context" + section = "Server Hooks" + validator = validate_callable(2) + type = callable + + def ssl_context(config, default_ssl_context_factory): + return default_ssl_context_factory() + + default = staticmethod(ssl_context) + desc = """\ + Called when SSLContext is needed. + + Allows customizing SSL context. + + The callable needs to accept an instance variable for the Config and + a factory function that returns default SSLContext which is initialized + with certificates, private key, cert_reqs, and ciphers according to + config and can be further customized by the callable. + The callable needs to return SSLContext object. + + Following example shows a configuration file that sets the minimum TLS version to 1.3: + + .. code-block:: python + + def ssl_context(conf, default_ssl_context_factory): + import ssl + context = default_ssl_context_factory() + context.minimum_version = ssl.TLSVersion.TLSv1_3 + return context + + .. versionadded:: 21.0 + """ + + +class ProxyProtocol(Setting): + name = "proxy_protocol" + section = "Server Mechanics" + cli = ["--proxy-protocol"] + validator = validate_bool + default = False + action = "store_true" + desc = """\ + Enable detect PROXY protocol (PROXY mode). + + Allow using HTTP and Proxy together. It may be useful for work with + stunnel as HTTPS frontend and Gunicorn as HTTP server. + + PROXY protocol: http://haproxy.1wt.eu/download/1.5/doc/proxy-protocol.txt + + Example for stunnel config:: + + [https] + protocol = proxy + accept = 443 + connect = 80 + cert = /etc/ssl/certs/stunnel.pem + key = /etc/ssl/certs/stunnel.key + """ + + +class ProxyAllowFrom(Setting): + name = "proxy_allow_ips" + section = "Server Mechanics" + cli = ["--proxy-allow-from"] + validator = validate_string_to_addr_list + default = "127.0.0.1,::1" + desc = """\ + Front-end's IPs from which allowed accept proxy requests (comma separated). + + Set to ``*`` to disable checking of front-end IPs. This is useful for setups + where you don't know in advance the IP address of front-end, but + instead have ensured via other means that only your + authorized front-ends can access Gunicorn. + + .. note:: + + This option does not affect UNIX socket connections. Connections not associated with + an IP address are treated as allowed, unconditionally. + """ + + +class KeyFile(Setting): + name = "keyfile" + section = "SSL" + cli = ["--keyfile"] + meta = "FILE" + validator = validate_string + default = None + desc = """\ + SSL key file + """ + + +class CertFile(Setting): + name = "certfile" + section = "SSL" + cli = ["--certfile"] + meta = "FILE" + validator = validate_string + default = None + desc = """\ + SSL certificate file + """ + + +class SSLVersion(Setting): + name = "ssl_version" + section = "SSL" + cli = ["--ssl-version"] + validator = validate_ssl_version + + if hasattr(ssl, "PROTOCOL_TLS"): + default = ssl.PROTOCOL_TLS + else: + default = ssl.PROTOCOL_SSLv23 + + default = ssl.PROTOCOL_SSLv23 + desc = """\ + SSL version to use (see stdlib ssl module's). + + .. deprecated:: 21.0 + The option is deprecated and it is currently ignored. Use :ref:`ssl-context` instead. + + ============= ============ + --ssl-version Description + ============= ============ + SSLv3 SSLv3 is not-secure and is strongly discouraged. + SSLv23 Alias for TLS. Deprecated in Python 3.6, use TLS. + TLS Negotiate highest possible version between client/server. + Can yield SSL. (Python 3.6+) + TLSv1 TLS 1.0 + TLSv1_1 TLS 1.1 (Python 3.4+) + TLSv1_2 TLS 1.2 (Python 3.4+) + TLS_SERVER Auto-negotiate the highest protocol version like TLS, + but only support server-side SSLSocket connections. + (Python 3.6+) + ============= ============ + + .. versionchanged:: 19.7 + The default value has been changed from ``ssl.PROTOCOL_TLSv1`` to + ``ssl.PROTOCOL_SSLv23``. + .. versionchanged:: 20.0 + This setting now accepts string names based on ``ssl.PROTOCOL_`` + constants. + .. versionchanged:: 20.0.1 + The default value has been changed from ``ssl.PROTOCOL_SSLv23`` to + ``ssl.PROTOCOL_TLS`` when Python >= 3.6 . + """ + + +class CertReqs(Setting): + name = "cert_reqs" + section = "SSL" + cli = ["--cert-reqs"] + validator = validate_pos_int + default = ssl.CERT_NONE + desc = """\ + Whether client certificate is required (see stdlib ssl module's) + + =========== =========================== + --cert-reqs Description + =========== =========================== + `0` no client verification + `1` ssl.CERT_OPTIONAL + `2` ssl.CERT_REQUIRED + =========== =========================== + """ + + +class CACerts(Setting): + name = "ca_certs" + section = "SSL" + cli = ["--ca-certs"] + meta = "FILE" + validator = validate_string + default = None + desc = """\ + CA certificates file + """ + + +class SuppressRaggedEOFs(Setting): + name = "suppress_ragged_eofs" + section = "SSL" + cli = ["--suppress-ragged-eofs"] + action = "store_true" + default = True + validator = validate_bool + desc = """\ + Suppress ragged EOFs (see stdlib ssl module's) + """ + + +class DoHandshakeOnConnect(Setting): + name = "do_handshake_on_connect" + section = "SSL" + cli = ["--do-handshake-on-connect"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Whether to perform SSL handshake on socket connect (see stdlib ssl module's) + """ + + +class Ciphers(Setting): + name = "ciphers" + section = "SSL" + cli = ["--ciphers"] + validator = validate_string + default = None + desc = """\ + SSL Cipher suite to use, in the format of an OpenSSL cipher list. + + By default we use the default cipher list from Python's ``ssl`` module, + which contains ciphers considered strong at the time of each Python + release. + + As a recommended alternative, the Open Web App Security Project (OWASP) + offers `a vetted set of strong cipher strings rated A+ to C- + `_. + OWASP provides details on user-agent compatibility at each security level. + + See the `OpenSSL Cipher List Format Documentation + `_ + for details on the format of an OpenSSL cipher list. + """ + + +class PasteGlobalConf(Setting): + name = "raw_paste_global_conf" + action = "append" + section = "Server Mechanics" + cli = ["--paste-global"] + meta = "CONF" + validator = validate_list_string + default = [] + + desc = """\ + Set a PasteDeploy global config variable in ``key=value`` form. + + The option can be specified multiple times. + + The variables are passed to the PasteDeploy entrypoint. Example:: + + $ gunicorn -b 127.0.0.1:8000 --paste development.ini --paste-global FOO=1 --paste-global BAR=2 + + .. versionadded:: 19.7 + """ + + +class PermitObsoleteFolding(Setting): + name = "permit_obsolete_folding" + section = "Server Mechanics" + cli = ["--permit-obsolete-folding"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Permit requests employing obsolete HTTP line folding mechanism + + The folding mechanism was deprecated by rfc7230 Section 3.2.4 and will not be + employed in HTTP request headers from standards-compliant HTTP clients. + + This option is provided to diagnose backwards-incompatible changes. + Use with care and only if necessary. Temporary; the precise effect of this option may + change in a future version, or it may be removed altogether. + + .. versionadded:: 23.0.0 + """ + + +class StripHeaderSpaces(Setting): + name = "strip_header_spaces" + section = "Server Mechanics" + cli = ["--strip-header-spaces"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Strip spaces present between the header name and the the ``:``. + + This is known to induce vulnerabilities and is not compliant with the HTTP/1.1 standard. + See https://portswigger.net/research/http-desync-attacks-request-smuggling-reborn. + + Use with care and only if necessary. Deprecated; scheduled for removal in 25.0.0 + + .. versionadded:: 20.0.1 + """ + + +class PermitUnconventionalHTTPMethod(Setting): + name = "permit_unconventional_http_method" + section = "Server Mechanics" + cli = ["--permit-unconventional-http-method"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Permit HTTP methods not matching conventions, such as IANA registration guidelines + + This permits request methods of length less than 3 or more than 20, + methods with lowercase characters or methods containing the # character. + HTTP methods are case sensitive by definition, and merely uppercase by convention. + + If unset, Gunicorn will apply nonstandard restrictions and cause 400 response status + in cases where otherwise 501 status is expected. While this option does modify that + behaviour, it should not be depended upon to guarantee standards-compliant behaviour. + Rather, it is provided temporarily, to assist in diagnosing backwards-incompatible + changes around the incomplete application of those restrictions. + + Use with care and only if necessary. Temporary; scheduled for removal in 24.0.0 + + .. versionadded:: 22.0.0 + """ + + +class PermitUnconventionalHTTPVersion(Setting): + name = "permit_unconventional_http_version" + section = "Server Mechanics" + cli = ["--permit-unconventional-http-version"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Permit HTTP version not matching conventions of 2023 + + This disables the refusal of likely malformed request lines. + It is unusual to specify HTTP 1 versions other than 1.0 and 1.1. + + This option is provided to diagnose backwards-incompatible changes. + Use with care and only if necessary. Temporary; the precise effect of this option may + change in a future version, or it may be removed altogether. + + .. versionadded:: 22.0.0 + """ + + +class CasefoldHTTPMethod(Setting): + name = "casefold_http_method" + section = "Server Mechanics" + cli = ["--casefold-http-method"] + validator = validate_bool + action = "store_true" + default = False + desc = """\ + Transform received HTTP methods to uppercase + + HTTP methods are case sensitive by definition, and merely uppercase by convention. + + This option is provided because previous versions of gunicorn defaulted to this behaviour. + + Use with care and only if necessary. Deprecated; scheduled for removal in 24.0.0 + + .. versionadded:: 22.0.0 + """ + + +def validate_header_map_behaviour(val): + # FIXME: refactor all of this subclassing stdlib argparse + + if val is None: + return + + if not isinstance(val, str): + raise TypeError("Invalid type for casting: %s" % val) + if val.lower().strip() == "drop": + return "drop" + elif val.lower().strip() == "refuse": + return "refuse" + elif val.lower().strip() == "dangerous": + return "dangerous" + else: + raise ValueError("Invalid header map behaviour: %s" % val) + + +class ForwarderHeaders(Setting): + name = "forwarder_headers" + section = "Server Mechanics" + cli = ["--forwarder-headers"] + validator = validate_string_to_list + default = "SCRIPT_NAME,PATH_INFO" + desc = """\ + + A list containing upper-case header field names that the front-end proxy + (see :ref:`forwarded-allow-ips`) sets, to be used in WSGI environment. + + This option has no effect for headers not present in the request. + + This option can be used to transfer ``SCRIPT_NAME``, ``PATH_INFO`` + and ``REMOTE_USER``. + + It is important that your front-end proxy configuration ensures that + the headers defined here can not be passed directly from the client. + """ + + +class HeaderMap(Setting): + name = "header_map" + section = "Server Mechanics" + cli = ["--header-map"] + validator = validate_header_map_behaviour + default = "drop" + desc = """\ + Configure how header field names are mapped into environ + + Headers containing underscores are permitted by RFC9110, + but gunicorn joining headers of different names into + the same environment variable will dangerously confuse applications as to which is which. + + The safe default ``drop`` is to silently drop headers that cannot be unambiguously mapped. + The value ``refuse`` will return an error if a request contains *any* such header. + The value ``dangerous`` matches the previous, not advisable, behaviour of mapping different + header field names into the same environ name. + + If the source is permitted as explained in :ref:`forwarded-allow-ips`, *and* the header name is + present in :ref:`forwarder-headers`, the header is mapped into environment regardless of + the state of this setting. + + Use with care and only if necessary and after considering if your problem could + instead be solved by specifically renaming or rewriting only the intended headers + on a proxy in front of Gunicorn. + + .. versionadded:: 22.0.0 + """ diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/debug.py b/netdeploy/lib/python3.11/site-packages/gunicorn/debug.py new file mode 100644 index 0000000..5fae0b4 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/debug.py @@ -0,0 +1,68 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +"""The debug module contains utilities and functions for better +debugging Gunicorn.""" + +import sys +import linecache +import re +import inspect + +__all__ = ['spew', 'unspew'] + +_token_spliter = re.compile(r'\W+') + + +class Spew: + + def __init__(self, trace_names=None, show_values=True): + self.trace_names = trace_names + self.show_values = show_values + + def __call__(self, frame, event, arg): + if event == 'line': + lineno = frame.f_lineno + if '__file__' in frame.f_globals: + filename = frame.f_globals['__file__'] + if (filename.endswith('.pyc') or + filename.endswith('.pyo')): + filename = filename[:-1] + name = frame.f_globals['__name__'] + line = linecache.getline(filename, lineno) + else: + name = '[unknown]' + try: + src = inspect.getsourcelines(frame) + line = src[lineno] + except OSError: + line = 'Unknown code named [%s]. VM instruction #%d' % ( + frame.f_code.co_name, frame.f_lasti) + if self.trace_names is None or name in self.trace_names: + print('%s:%s: %s' % (name, lineno, line.rstrip())) + if not self.show_values: + return self + details = [] + tokens = _token_spliter.split(line) + for tok in tokens: + if tok in frame.f_globals: + details.append('%s=%r' % (tok, frame.f_globals[tok])) + if tok in frame.f_locals: + details.append('%s=%r' % (tok, frame.f_locals[tok])) + if details: + print("\t%s" % ' '.join(details)) + return self + + +def spew(trace_names=None, show_values=False): + """Install a trace hook which writes incredibly detailed logs + about what code is being executed to stdout. + """ + sys.settrace(Spew(trace_names, show_values)) + + +def unspew(): + """Remove the trace hook installed by spew. + """ + sys.settrace(None) diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/errors.py b/netdeploy/lib/python3.11/site-packages/gunicorn/errors.py new file mode 100644 index 0000000..1128380 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/errors.py @@ -0,0 +1,28 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +# We don't need to call super() in __init__ methods of our +# BaseException and Exception classes because we also define +# our own __str__ methods so there is no need to pass 'message' +# to the base class to get a meaningful output from 'str(exc)'. +# pylint: disable=super-init-not-called + + +# we inherit from BaseException here to make sure to not be caught +# at application level +class HaltServer(BaseException): + def __init__(self, reason, exit_status=1): + self.reason = reason + self.exit_status = exit_status + + def __str__(self): + return "" % (self.reason, self.exit_status) + + +class ConfigError(Exception): + """ Exception raised on config error """ + + +class AppImportError(Exception): + """ Exception raised when loading an application """ diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/glogging.py b/netdeploy/lib/python3.11/site-packages/gunicorn/glogging.py new file mode 100644 index 0000000..e34fcd5 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/glogging.py @@ -0,0 +1,473 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import base64 +import binascii +import json +import time +import logging +logging.Logger.manager.emittedNoHandlerWarning = 1 # noqa +from logging.config import dictConfig +from logging.config import fileConfig +import os +import socket +import sys +import threading +import traceback + +from gunicorn import util + + +# syslog facility codes +SYSLOG_FACILITIES = { + "auth": 4, + "authpriv": 10, + "cron": 9, + "daemon": 3, + "ftp": 11, + "kern": 0, + "lpr": 6, + "mail": 2, + "news": 7, + "security": 4, # DEPRECATED + "syslog": 5, + "user": 1, + "uucp": 8, + "local0": 16, + "local1": 17, + "local2": 18, + "local3": 19, + "local4": 20, + "local5": 21, + "local6": 22, + "local7": 23 +} + +CONFIG_DEFAULTS = { + "version": 1, + "disable_existing_loggers": False, + "root": {"level": "INFO", "handlers": ["console"]}, + "loggers": { + "gunicorn.error": { + "level": "INFO", + "handlers": ["error_console"], + "propagate": True, + "qualname": "gunicorn.error" + }, + + "gunicorn.access": { + "level": "INFO", + "handlers": ["console"], + "propagate": True, + "qualname": "gunicorn.access" + } + }, + "handlers": { + "console": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "ext://sys.stdout" + }, + "error_console": { + "class": "logging.StreamHandler", + "formatter": "generic", + "stream": "ext://sys.stderr" + }, + }, + "formatters": { + "generic": { + "format": "%(asctime)s [%(process)d] [%(levelname)s] %(message)s", + "datefmt": "[%Y-%m-%d %H:%M:%S %z]", + "class": "logging.Formatter" + } + } +} + + +def loggers(): + """ get list of all loggers """ + root = logging.root + existing = list(root.manager.loggerDict.keys()) + return [logging.getLogger(name) for name in existing] + + +class SafeAtoms(dict): + + def __init__(self, atoms): + dict.__init__(self) + for key, value in atoms.items(): + if isinstance(value, str): + self[key] = value.replace('"', '\\"') + else: + self[key] = value + + def __getitem__(self, k): + if k.startswith("{"): + kl = k.lower() + if kl in self: + return super().__getitem__(kl) + else: + return "-" + if k in self: + return super().__getitem__(k) + else: + return '-' + + +def parse_syslog_address(addr): + + # unix domain socket type depends on backend + # SysLogHandler will try both when given None + if addr.startswith("unix://"): + sock_type = None + + # set socket type only if explicitly requested + parts = addr.split("#", 1) + if len(parts) == 2: + addr = parts[0] + if parts[1] == "dgram": + sock_type = socket.SOCK_DGRAM + + return (sock_type, addr.split("unix://")[1]) + + if addr.startswith("udp://"): + addr = addr.split("udp://")[1] + socktype = socket.SOCK_DGRAM + elif addr.startswith("tcp://"): + addr = addr.split("tcp://")[1] + socktype = socket.SOCK_STREAM + else: + raise RuntimeError("invalid syslog address") + + if '[' in addr and ']' in addr: + host = addr.split(']')[0][1:].lower() + elif ':' in addr: + host = addr.split(':')[0].lower() + elif addr == "": + host = "localhost" + else: + host = addr.lower() + + addr = addr.split(']')[-1] + if ":" in addr: + port = addr.split(':', 1)[1] + if not port.isdigit(): + raise RuntimeError("%r is not a valid port number." % port) + port = int(port) + else: + port = 514 + + return (socktype, (host, port)) + + +class Logger: + + LOG_LEVELS = { + "critical": logging.CRITICAL, + "error": logging.ERROR, + "warning": logging.WARNING, + "info": logging.INFO, + "debug": logging.DEBUG + } + loglevel = logging.INFO + + error_fmt = r"%(asctime)s [%(process)d] [%(levelname)s] %(message)s" + datefmt = r"[%Y-%m-%d %H:%M:%S %z]" + + access_fmt = "%(message)s" + syslog_fmt = "[%(process)d] %(message)s" + + atoms_wrapper_class = SafeAtoms + + def __init__(self, cfg): + self.error_log = logging.getLogger("gunicorn.error") + self.error_log.propagate = False + self.access_log = logging.getLogger("gunicorn.access") + self.access_log.propagate = False + self.error_handlers = [] + self.access_handlers = [] + self.logfile = None + self.lock = threading.Lock() + self.cfg = cfg + self.setup(cfg) + + def setup(self, cfg): + self.loglevel = self.LOG_LEVELS.get(cfg.loglevel.lower(), logging.INFO) + self.error_log.setLevel(self.loglevel) + self.access_log.setLevel(logging.INFO) + + # set gunicorn.error handler + if self.cfg.capture_output and cfg.errorlog != "-": + for stream in sys.stdout, sys.stderr: + stream.flush() + + self.logfile = open(cfg.errorlog, 'a+') + os.dup2(self.logfile.fileno(), sys.stdout.fileno()) + os.dup2(self.logfile.fileno(), sys.stderr.fileno()) + + self._set_handler(self.error_log, cfg.errorlog, + logging.Formatter(self.error_fmt, self.datefmt)) + + # set gunicorn.access handler + if cfg.accesslog is not None: + self._set_handler( + self.access_log, cfg.accesslog, + fmt=logging.Formatter(self.access_fmt), stream=sys.stdout + ) + + # set syslog handler + if cfg.syslog: + self._set_syslog_handler( + self.error_log, cfg, self.syslog_fmt, "error" + ) + if not cfg.disable_redirect_access_to_syslog: + self._set_syslog_handler( + self.access_log, cfg, self.syslog_fmt, "access" + ) + + if cfg.logconfig_dict: + config = CONFIG_DEFAULTS.copy() + config.update(cfg.logconfig_dict) + try: + dictConfig(config) + except ( + AttributeError, + ImportError, + ValueError, + TypeError + ) as exc: + raise RuntimeError(str(exc)) + elif cfg.logconfig_json: + config = CONFIG_DEFAULTS.copy() + if os.path.exists(cfg.logconfig_json): + try: + config_json = json.load(open(cfg.logconfig_json)) + config.update(config_json) + dictConfig(config) + except ( + json.JSONDecodeError, + AttributeError, + ImportError, + ValueError, + TypeError + ) as exc: + raise RuntimeError(str(exc)) + elif cfg.logconfig: + if os.path.exists(cfg.logconfig): + defaults = CONFIG_DEFAULTS.copy() + defaults['__file__'] = cfg.logconfig + defaults['here'] = os.path.dirname(cfg.logconfig) + fileConfig(cfg.logconfig, defaults=defaults, + disable_existing_loggers=False) + else: + msg = "Error: log config '%s' not found" + raise RuntimeError(msg % cfg.logconfig) + + def critical(self, msg, *args, **kwargs): + self.error_log.critical(msg, *args, **kwargs) + + def error(self, msg, *args, **kwargs): + self.error_log.error(msg, *args, **kwargs) + + def warning(self, msg, *args, **kwargs): + self.error_log.warning(msg, *args, **kwargs) + + def info(self, msg, *args, **kwargs): + self.error_log.info(msg, *args, **kwargs) + + def debug(self, msg, *args, **kwargs): + self.error_log.debug(msg, *args, **kwargs) + + def exception(self, msg, *args, **kwargs): + self.error_log.exception(msg, *args, **kwargs) + + def log(self, lvl, msg, *args, **kwargs): + if isinstance(lvl, str): + lvl = self.LOG_LEVELS.get(lvl.lower(), logging.INFO) + self.error_log.log(lvl, msg, *args, **kwargs) + + def atoms(self, resp, req, environ, request_time): + """ Gets atoms for log formatting. + """ + status = resp.status + if isinstance(status, str): + status = status.split(None, 1)[0] + atoms = { + 'h': environ.get('REMOTE_ADDR', '-'), + 'l': '-', + 'u': self._get_user(environ) or '-', + 't': self.now(), + 'r': "%s %s %s" % (environ['REQUEST_METHOD'], + environ['RAW_URI'], + environ["SERVER_PROTOCOL"]), + 's': status, + 'm': environ.get('REQUEST_METHOD'), + 'U': environ.get('PATH_INFO'), + 'q': environ.get('QUERY_STRING'), + 'H': environ.get('SERVER_PROTOCOL'), + 'b': getattr(resp, 'sent', None) is not None and str(resp.sent) or '-', + 'B': getattr(resp, 'sent', None), + 'f': environ.get('HTTP_REFERER', '-'), + 'a': environ.get('HTTP_USER_AGENT', '-'), + 'T': request_time.seconds, + 'D': (request_time.seconds * 1000000) + request_time.microseconds, + 'M': (request_time.seconds * 1000) + int(request_time.microseconds / 1000), + 'L': "%d.%06d" % (request_time.seconds, request_time.microseconds), + 'p': "<%s>" % os.getpid() + } + + # add request headers + if hasattr(req, 'headers'): + req_headers = req.headers + else: + req_headers = req + + if hasattr(req_headers, "items"): + req_headers = req_headers.items() + + atoms.update({"{%s}i" % k.lower(): v for k, v in req_headers}) + + resp_headers = resp.headers + if hasattr(resp_headers, "items"): + resp_headers = resp_headers.items() + + # add response headers + atoms.update({"{%s}o" % k.lower(): v for k, v in resp_headers}) + + # add environ variables + environ_variables = environ.items() + atoms.update({"{%s}e" % k.lower(): v for k, v in environ_variables}) + + return atoms + + def access(self, resp, req, environ, request_time): + """ See http://httpd.apache.org/docs/2.0/logs.html#combined + for format details + """ + + if not (self.cfg.accesslog or self.cfg.logconfig or + self.cfg.logconfig_dict or self.cfg.logconfig_json or + (self.cfg.syslog and not self.cfg.disable_redirect_access_to_syslog)): + return + + # wrap atoms: + # - make sure atoms will be test case insensitively + # - if atom doesn't exist replace it by '-' + safe_atoms = self.atoms_wrapper_class( + self.atoms(resp, req, environ, request_time) + ) + + try: + self.access_log.info(self.cfg.access_log_format, safe_atoms) + except Exception: + self.error(traceback.format_exc()) + + def now(self): + """ return date in Apache Common Log Format """ + return time.strftime('[%d/%b/%Y:%H:%M:%S %z]') + + def reopen_files(self): + if self.cfg.capture_output and self.cfg.errorlog != "-": + for stream in sys.stdout, sys.stderr: + stream.flush() + + with self.lock: + if self.logfile is not None: + self.logfile.close() + self.logfile = open(self.cfg.errorlog, 'a+') + os.dup2(self.logfile.fileno(), sys.stdout.fileno()) + os.dup2(self.logfile.fileno(), sys.stderr.fileno()) + + for log in loggers(): + for handler in log.handlers: + if isinstance(handler, logging.FileHandler): + handler.acquire() + try: + if handler.stream: + handler.close() + handler.stream = handler._open() + finally: + handler.release() + + def close_on_exec(self): + for log in loggers(): + for handler in log.handlers: + if isinstance(handler, logging.FileHandler): + handler.acquire() + try: + if handler.stream: + util.close_on_exec(handler.stream.fileno()) + finally: + handler.release() + + def _get_gunicorn_handler(self, log): + for h in log.handlers: + if getattr(h, "_gunicorn", False): + return h + + def _set_handler(self, log, output, fmt, stream=None): + # remove previous gunicorn log handler + h = self._get_gunicorn_handler(log) + if h: + log.handlers.remove(h) + + if output is not None: + if output == "-": + h = logging.StreamHandler(stream) + else: + util.check_is_writable(output) + h = logging.FileHandler(output) + # make sure the user can reopen the file + try: + os.chown(h.baseFilename, self.cfg.user, self.cfg.group) + except OSError: + # it's probably OK there, we assume the user has given + # /dev/null as a parameter. + pass + + h.setFormatter(fmt) + h._gunicorn = True + log.addHandler(h) + + def _set_syslog_handler(self, log, cfg, fmt, name): + # setup format + prefix = cfg.syslog_prefix or cfg.proc_name.replace(":", ".") + + prefix = "gunicorn.%s.%s" % (prefix, name) + + # set format + fmt = logging.Formatter(r"%s: %s" % (prefix, fmt)) + + # syslog facility + try: + facility = SYSLOG_FACILITIES[cfg.syslog_facility.lower()] + except KeyError: + raise RuntimeError("unknown facility name") + + # parse syslog address + socktype, addr = parse_syslog_address(cfg.syslog_addr) + + # finally setup the syslog handler + h = logging.handlers.SysLogHandler(address=addr, + facility=facility, socktype=socktype) + + h.setFormatter(fmt) + h._gunicorn = True + log.addHandler(h) + + def _get_user(self, environ): + user = None + http_auth = environ.get("HTTP_AUTHORIZATION") + if http_auth and http_auth.lower().startswith('basic'): + auth = http_auth.split(" ", 1) + if len(auth) == 2: + try: + # b64decode doesn't accept unicode in Python < 3.3 + # so we need to convert it to a byte string + auth = base64.b64decode(auth[1].strip().encode('utf-8')) + # b64decode returns a byte string + user = auth.split(b":", 1)[0].decode("UTF-8") + except (TypeError, binascii.Error, UnicodeDecodeError) as exc: + self.debug("Couldn't get username: %s", exc) + return user diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/http/__init__.py b/netdeploy/lib/python3.11/site-packages/gunicorn/http/__init__.py new file mode 100644 index 0000000..11473bb --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/http/__init__.py @@ -0,0 +1,8 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +from gunicorn.http.message import Message, Request +from gunicorn.http.parser import RequestParser + +__all__ = ['Message', 'Request', 'RequestParser'] diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/http/body.py b/netdeploy/lib/python3.11/site-packages/gunicorn/http/body.py new file mode 100644 index 0000000..d7ee29e --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/http/body.py @@ -0,0 +1,268 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import io +import sys + +from gunicorn.http.errors import (NoMoreData, ChunkMissingTerminator, + InvalidChunkSize) + + +class ChunkedReader: + def __init__(self, req, unreader): + self.req = req + self.parser = self.parse_chunked(unreader) + self.buf = io.BytesIO() + + def read(self, size): + if not isinstance(size, int): + raise TypeError("size must be an integer type") + if size < 0: + raise ValueError("Size must be positive.") + if size == 0: + return b"" + + if self.parser: + while self.buf.tell() < size: + try: + self.buf.write(next(self.parser)) + except StopIteration: + self.parser = None + break + + data = self.buf.getvalue() + ret, rest = data[:size], data[size:] + self.buf = io.BytesIO() + self.buf.write(rest) + return ret + + def parse_trailers(self, unreader, data): + buf = io.BytesIO() + buf.write(data) + + idx = buf.getvalue().find(b"\r\n\r\n") + done = buf.getvalue()[:2] == b"\r\n" + while idx < 0 and not done: + self.get_data(unreader, buf) + idx = buf.getvalue().find(b"\r\n\r\n") + done = buf.getvalue()[:2] == b"\r\n" + if done: + unreader.unread(buf.getvalue()[2:]) + return b"" + self.req.trailers = self.req.parse_headers(buf.getvalue()[:idx], from_trailer=True) + unreader.unread(buf.getvalue()[idx + 4:]) + + def parse_chunked(self, unreader): + (size, rest) = self.parse_chunk_size(unreader) + while size > 0: + while size > len(rest): + size -= len(rest) + yield rest + rest = unreader.read() + if not rest: + raise NoMoreData() + yield rest[:size] + # Remove \r\n after chunk + rest = rest[size:] + while len(rest) < 2: + new_data = unreader.read() + if not new_data: + break + rest += new_data + if rest[:2] != b'\r\n': + raise ChunkMissingTerminator(rest[:2]) + (size, rest) = self.parse_chunk_size(unreader, data=rest[2:]) + + def parse_chunk_size(self, unreader, data=None): + buf = io.BytesIO() + if data is not None: + buf.write(data) + + idx = buf.getvalue().find(b"\r\n") + while idx < 0: + self.get_data(unreader, buf) + idx = buf.getvalue().find(b"\r\n") + + data = buf.getvalue() + line, rest_chunk = data[:idx], data[idx + 2:] + + # RFC9112 7.1.1: BWS before chunk-ext - but ONLY then + chunk_size, *chunk_ext = line.split(b";", 1) + if chunk_ext: + chunk_size = chunk_size.rstrip(b" \t") + if any(n not in b"0123456789abcdefABCDEF" for n in chunk_size): + raise InvalidChunkSize(chunk_size) + if len(chunk_size) == 0: + raise InvalidChunkSize(chunk_size) + chunk_size = int(chunk_size, 16) + + if chunk_size == 0: + try: + self.parse_trailers(unreader, rest_chunk) + except NoMoreData: + pass + return (0, None) + return (chunk_size, rest_chunk) + + def get_data(self, unreader, buf): + data = unreader.read() + if not data: + raise NoMoreData() + buf.write(data) + + +class LengthReader: + def __init__(self, unreader, length): + self.unreader = unreader + self.length = length + + def read(self, size): + if not isinstance(size, int): + raise TypeError("size must be an integral type") + + size = min(self.length, size) + if size < 0: + raise ValueError("Size must be positive.") + if size == 0: + return b"" + + buf = io.BytesIO() + data = self.unreader.read() + while data: + buf.write(data) + if buf.tell() >= size: + break + data = self.unreader.read() + + buf = buf.getvalue() + ret, rest = buf[:size], buf[size:] + self.unreader.unread(rest) + self.length -= size + return ret + + +class EOFReader: + def __init__(self, unreader): + self.unreader = unreader + self.buf = io.BytesIO() + self.finished = False + + def read(self, size): + if not isinstance(size, int): + raise TypeError("size must be an integral type") + if size < 0: + raise ValueError("Size must be positive.") + if size == 0: + return b"" + + if self.finished: + data = self.buf.getvalue() + ret, rest = data[:size], data[size:] + self.buf = io.BytesIO() + self.buf.write(rest) + return ret + + data = self.unreader.read() + while data: + self.buf.write(data) + if self.buf.tell() > size: + break + data = self.unreader.read() + + if not data: + self.finished = True + + data = self.buf.getvalue() + ret, rest = data[:size], data[size:] + self.buf = io.BytesIO() + self.buf.write(rest) + return ret + + +class Body: + def __init__(self, reader): + self.reader = reader + self.buf = io.BytesIO() + + def __iter__(self): + return self + + def __next__(self): + ret = self.readline() + if not ret: + raise StopIteration() + return ret + + next = __next__ + + def getsize(self, size): + if size is None: + return sys.maxsize + elif not isinstance(size, int): + raise TypeError("size must be an integral type") + elif size < 0: + return sys.maxsize + return size + + def read(self, size=None): + size = self.getsize(size) + if size == 0: + return b"" + + if size < self.buf.tell(): + data = self.buf.getvalue() + ret, rest = data[:size], data[size:] + self.buf = io.BytesIO() + self.buf.write(rest) + return ret + + while size > self.buf.tell(): + data = self.reader.read(1024) + if not data: + break + self.buf.write(data) + + data = self.buf.getvalue() + ret, rest = data[:size], data[size:] + self.buf = io.BytesIO() + self.buf.write(rest) + return ret + + def readline(self, size=None): + size = self.getsize(size) + if size == 0: + return b"" + + data = self.buf.getvalue() + self.buf = io.BytesIO() + + ret = [] + while 1: + idx = data.find(b"\n", 0, size) + idx = idx + 1 if idx >= 0 else size if len(data) >= size else 0 + if idx: + ret.append(data[:idx]) + self.buf.write(data[idx:]) + break + + ret.append(data) + size -= len(data) + data = self.reader.read(min(1024, size)) + if not data: + break + + return b"".join(ret) + + def readlines(self, size=None): + ret = [] + data = self.read() + while data: + pos = data.find(b"\n") + if pos < 0: + ret.append(data) + data = b"" + else: + line, data = data[:pos + 1], data[pos + 1:] + ret.append(line) + return ret diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/http/errors.py b/netdeploy/lib/python3.11/site-packages/gunicorn/http/errors.py new file mode 100644 index 0000000..bcb9700 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/http/errors.py @@ -0,0 +1,145 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +# We don't need to call super() in __init__ methods of our +# BaseException and Exception classes because we also define +# our own __str__ methods so there is no need to pass 'message' +# to the base class to get a meaningful output from 'str(exc)'. +# pylint: disable=super-init-not-called + + +class ParseException(Exception): + pass + + +class NoMoreData(IOError): + def __init__(self, buf=None): + self.buf = buf + + def __str__(self): + return "No more data after: %r" % self.buf + + +class ConfigurationProblem(ParseException): + def __init__(self, info): + self.info = info + self.code = 500 + + def __str__(self): + return "Configuration problem: %s" % self.info + + +class InvalidRequestLine(ParseException): + def __init__(self, req): + self.req = req + self.code = 400 + + def __str__(self): + return "Invalid HTTP request line: %r" % self.req + + +class InvalidRequestMethod(ParseException): + def __init__(self, method): + self.method = method + + def __str__(self): + return "Invalid HTTP method: %r" % self.method + + +class InvalidHTTPVersion(ParseException): + def __init__(self, version): + self.version = version + + def __str__(self): + return "Invalid HTTP Version: %r" % (self.version,) + + +class InvalidHeader(ParseException): + def __init__(self, hdr, req=None): + self.hdr = hdr + self.req = req + + def __str__(self): + return "Invalid HTTP Header: %r" % self.hdr + + +class ObsoleteFolding(ParseException): + def __init__(self, hdr): + self.hdr = hdr + + def __str__(self): + return "Obsolete line folding is unacceptable: %r" % (self.hdr, ) + + +class InvalidHeaderName(ParseException): + def __init__(self, hdr): + self.hdr = hdr + + def __str__(self): + return "Invalid HTTP header name: %r" % self.hdr + + +class UnsupportedTransferCoding(ParseException): + def __init__(self, hdr): + self.hdr = hdr + self.code = 501 + + def __str__(self): + return "Unsupported transfer coding: %r" % self.hdr + + +class InvalidChunkSize(IOError): + def __init__(self, data): + self.data = data + + def __str__(self): + return "Invalid chunk size: %r" % self.data + + +class ChunkMissingTerminator(IOError): + def __init__(self, term): + self.term = term + + def __str__(self): + return "Invalid chunk terminator is not '\\r\\n': %r" % self.term + + +class LimitRequestLine(ParseException): + def __init__(self, size, max_size): + self.size = size + self.max_size = max_size + + def __str__(self): + return "Request Line is too large (%s > %s)" % (self.size, self.max_size) + + +class LimitRequestHeaders(ParseException): + def __init__(self, msg): + self.msg = msg + + def __str__(self): + return self.msg + + +class InvalidProxyLine(ParseException): + def __init__(self, line): + self.line = line + self.code = 400 + + def __str__(self): + return "Invalid PROXY line: %r" % self.line + + +class ForbiddenProxyRequest(ParseException): + def __init__(self, host): + self.host = host + self.code = 403 + + def __str__(self): + return "Proxy request from %r not allowed" % self.host + + +class InvalidSchemeHeaders(ParseException): + def __str__(self): + return "Contradictory scheme headers" diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/http/message.py b/netdeploy/lib/python3.11/site-packages/gunicorn/http/message.py new file mode 100644 index 0000000..59ce0bf --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/http/message.py @@ -0,0 +1,463 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import io +import re +import socket + +from gunicorn.http.body import ChunkedReader, LengthReader, EOFReader, Body +from gunicorn.http.errors import ( + InvalidHeader, InvalidHeaderName, NoMoreData, + InvalidRequestLine, InvalidRequestMethod, InvalidHTTPVersion, + LimitRequestLine, LimitRequestHeaders, + UnsupportedTransferCoding, ObsoleteFolding, +) +from gunicorn.http.errors import InvalidProxyLine, ForbiddenProxyRequest +from gunicorn.http.errors import InvalidSchemeHeaders +from gunicorn.util import bytes_to_str, split_request_uri + +MAX_REQUEST_LINE = 8190 +MAX_HEADERS = 32768 +DEFAULT_MAX_HEADERFIELD_SIZE = 8190 + +# verbosely on purpose, avoid backslash ambiguity +RFC9110_5_6_2_TOKEN_SPECIALS = r"!#$%&'*+-.^_`|~" +TOKEN_RE = re.compile(r"[%s0-9a-zA-Z]+" % (re.escape(RFC9110_5_6_2_TOKEN_SPECIALS))) +METHOD_BADCHAR_RE = re.compile("[a-z#]") +# usually 1.0 or 1.1 - RFC9112 permits restricting to single-digit versions +VERSION_RE = re.compile(r"HTTP/(\d)\.(\d)") +RFC9110_5_5_INVALID_AND_DANGEROUS = re.compile(r"[\0\r\n]") + + +class Message: + def __init__(self, cfg, unreader, peer_addr): + self.cfg = cfg + self.unreader = unreader + self.peer_addr = peer_addr + self.remote_addr = peer_addr + self.version = None + self.headers = [] + self.trailers = [] + self.body = None + self.scheme = "https" if cfg.is_ssl else "http" + self.must_close = False + + # set headers limits + self.limit_request_fields = cfg.limit_request_fields + if (self.limit_request_fields <= 0 + or self.limit_request_fields > MAX_HEADERS): + self.limit_request_fields = MAX_HEADERS + self.limit_request_field_size = cfg.limit_request_field_size + if self.limit_request_field_size < 0: + self.limit_request_field_size = DEFAULT_MAX_HEADERFIELD_SIZE + + # set max header buffer size + max_header_field_size = self.limit_request_field_size or DEFAULT_MAX_HEADERFIELD_SIZE + self.max_buffer_headers = self.limit_request_fields * \ + (max_header_field_size + 2) + 4 + + unused = self.parse(self.unreader) + self.unreader.unread(unused) + self.set_body_reader() + + def force_close(self): + self.must_close = True + + def parse(self, unreader): + raise NotImplementedError() + + def parse_headers(self, data, from_trailer=False): + cfg = self.cfg + headers = [] + + # Split lines on \r\n + lines = [bytes_to_str(line) for line in data.split(b"\r\n")] + + # handle scheme headers + scheme_header = False + secure_scheme_headers = {} + forwarder_headers = [] + if from_trailer: + # nonsense. either a request is https from the beginning + # .. or we are just behind a proxy who does not remove conflicting trailers + pass + elif ('*' in cfg.forwarded_allow_ips or + not isinstance(self.peer_addr, tuple) + or self.peer_addr[0] in cfg.forwarded_allow_ips): + secure_scheme_headers = cfg.secure_scheme_headers + forwarder_headers = cfg.forwarder_headers + + # Parse headers into key/value pairs paying attention + # to continuation lines. + while lines: + if len(headers) >= self.limit_request_fields: + raise LimitRequestHeaders("limit request headers fields") + + # Parse initial header name: value pair. + curr = lines.pop(0) + header_length = len(curr) + len("\r\n") + if curr.find(":") <= 0: + raise InvalidHeader(curr) + name, value = curr.split(":", 1) + if self.cfg.strip_header_spaces: + name = name.rstrip(" \t") + if not TOKEN_RE.fullmatch(name): + raise InvalidHeaderName(name) + + # this is still a dangerous place to do this + # but it is more correct than doing it before the pattern match: + # after we entered Unicode wonderland, 8bits could case-shift into ASCII: + # b"\xDF".decode("latin-1").upper().encode("ascii") == b"SS" + name = name.upper() + + value = [value.strip(" \t")] + + # Consume value continuation lines.. + while lines and lines[0].startswith((" ", "\t")): + # .. which is obsolete here, and no longer done by default + if not self.cfg.permit_obsolete_folding: + raise ObsoleteFolding(name) + curr = lines.pop(0) + header_length += len(curr) + len("\r\n") + if header_length > self.limit_request_field_size > 0: + raise LimitRequestHeaders("limit request headers " + "fields size") + value.append(curr.strip("\t ")) + value = " ".join(value) + + if RFC9110_5_5_INVALID_AND_DANGEROUS.search(value): + raise InvalidHeader(name) + + if header_length > self.limit_request_field_size > 0: + raise LimitRequestHeaders("limit request headers fields size") + + if name in secure_scheme_headers: + secure = value == secure_scheme_headers[name] + scheme = "https" if secure else "http" + if scheme_header: + if scheme != self.scheme: + raise InvalidSchemeHeaders() + else: + scheme_header = True + self.scheme = scheme + + # ambiguous mapping allows fooling downstream, e.g. merging non-identical headers: + # X-Forwarded-For: 2001:db8::ha:cc:ed + # X_Forwarded_For: 127.0.0.1,::1 + # HTTP_X_FORWARDED_FOR = 2001:db8::ha:cc:ed,127.0.0.1,::1 + # Only modify after fixing *ALL* header transformations; network to wsgi env + if "_" in name: + if name in forwarder_headers or "*" in forwarder_headers: + # This forwarder may override our environment + pass + elif self.cfg.header_map == "dangerous": + # as if we did not know we cannot safely map this + pass + elif self.cfg.header_map == "drop": + # almost as if it never had been there + # but still counts against resource limits + continue + else: + # fail-safe fallthrough: refuse + raise InvalidHeaderName(name) + + headers.append((name, value)) + + return headers + + def set_body_reader(self): + chunked = False + content_length = None + + for (name, value) in self.headers: + if name == "CONTENT-LENGTH": + if content_length is not None: + raise InvalidHeader("CONTENT-LENGTH", req=self) + content_length = value + elif name == "TRANSFER-ENCODING": + # T-E can be a list + # https://datatracker.ietf.org/doc/html/rfc9112#name-transfer-encoding + vals = [v.strip() for v in value.split(',')] + for val in vals: + if val.lower() == "chunked": + # DANGER: transfer codings stack, and stacked chunking is never intended + if chunked: + raise InvalidHeader("TRANSFER-ENCODING", req=self) + chunked = True + elif val.lower() == "identity": + # does not do much, could still plausibly desync from what the proxy does + # safe option: nuke it, its never needed + if chunked: + raise InvalidHeader("TRANSFER-ENCODING", req=self) + elif val.lower() in ('compress', 'deflate', 'gzip'): + # chunked should be the last one + if chunked: + raise InvalidHeader("TRANSFER-ENCODING", req=self) + self.force_close() + else: + raise UnsupportedTransferCoding(value) + + if chunked: + # two potentially dangerous cases: + # a) CL + TE (TE overrides CL.. only safe if the recipient sees it that way too) + # b) chunked HTTP/1.0 (always faulty) + if self.version < (1, 1): + # framing wonky, see RFC 9112 Section 6.1 + raise InvalidHeader("TRANSFER-ENCODING", req=self) + if content_length is not None: + # we cannot be certain the message framing we understood matches proxy intent + # -> whatever happens next, remaining input must not be trusted + raise InvalidHeader("CONTENT-LENGTH", req=self) + self.body = Body(ChunkedReader(self, self.unreader)) + elif content_length is not None: + try: + if str(content_length).isnumeric(): + content_length = int(content_length) + else: + raise InvalidHeader("CONTENT-LENGTH", req=self) + except ValueError: + raise InvalidHeader("CONTENT-LENGTH", req=self) + + if content_length < 0: + raise InvalidHeader("CONTENT-LENGTH", req=self) + + self.body = Body(LengthReader(self.unreader, content_length)) + else: + self.body = Body(EOFReader(self.unreader)) + + def should_close(self): + if self.must_close: + return True + for (h, v) in self.headers: + if h == "CONNECTION": + v = v.lower().strip(" \t") + if v == "close": + return True + elif v == "keep-alive": + return False + break + return self.version <= (1, 0) + + +class Request(Message): + def __init__(self, cfg, unreader, peer_addr, req_number=1): + self.method = None + self.uri = None + self.path = None + self.query = None + self.fragment = None + + # get max request line size + self.limit_request_line = cfg.limit_request_line + if (self.limit_request_line < 0 + or self.limit_request_line >= MAX_REQUEST_LINE): + self.limit_request_line = MAX_REQUEST_LINE + + self.req_number = req_number + self.proxy_protocol_info = None + super().__init__(cfg, unreader, peer_addr) + + def get_data(self, unreader, buf, stop=False): + data = unreader.read() + if not data: + if stop: + raise StopIteration() + raise NoMoreData(buf.getvalue()) + buf.write(data) + + def parse(self, unreader): + buf = io.BytesIO() + self.get_data(unreader, buf, stop=True) + + # get request line + line, rbuf = self.read_line(unreader, buf, self.limit_request_line) + + # proxy protocol + if self.proxy_protocol(bytes_to_str(line)): + # get next request line + buf = io.BytesIO() + buf.write(rbuf) + line, rbuf = self.read_line(unreader, buf, self.limit_request_line) + + self.parse_request_line(line) + buf = io.BytesIO() + buf.write(rbuf) + + # Headers + data = buf.getvalue() + idx = data.find(b"\r\n\r\n") + + done = data[:2] == b"\r\n" + while True: + idx = data.find(b"\r\n\r\n") + done = data[:2] == b"\r\n" + + if idx < 0 and not done: + self.get_data(unreader, buf) + data = buf.getvalue() + if len(data) > self.max_buffer_headers: + raise LimitRequestHeaders("max buffer headers") + else: + break + + if done: + self.unreader.unread(data[2:]) + return b"" + + self.headers = self.parse_headers(data[:idx], from_trailer=False) + + ret = data[idx + 4:] + buf = None + return ret + + def read_line(self, unreader, buf, limit=0): + data = buf.getvalue() + + while True: + idx = data.find(b"\r\n") + if idx >= 0: + # check if the request line is too large + if idx > limit > 0: + raise LimitRequestLine(idx, limit) + break + if len(data) - 2 > limit > 0: + raise LimitRequestLine(len(data), limit) + self.get_data(unreader, buf) + data = buf.getvalue() + + return (data[:idx], # request line, + data[idx + 2:]) # residue in the buffer, skip \r\n + + def proxy_protocol(self, line): + """\ + Detect, check and parse proxy protocol. + + :raises: ForbiddenProxyRequest, InvalidProxyLine. + :return: True for proxy protocol line else False + """ + if not self.cfg.proxy_protocol: + return False + + if self.req_number != 1: + return False + + if not line.startswith("PROXY"): + return False + + self.proxy_protocol_access_check() + self.parse_proxy_protocol(line) + + return True + + def proxy_protocol_access_check(self): + # check in allow list + if ("*" not in self.cfg.proxy_allow_ips and + isinstance(self.peer_addr, tuple) and + self.peer_addr[0] not in self.cfg.proxy_allow_ips): + raise ForbiddenProxyRequest(self.peer_addr[0]) + + def parse_proxy_protocol(self, line): + bits = line.split(" ") + + if len(bits) != 6: + raise InvalidProxyLine(line) + + # Extract data + proto = bits[1] + s_addr = bits[2] + d_addr = bits[3] + + # Validation + if proto not in ["TCP4", "TCP6"]: + raise InvalidProxyLine("protocol '%s' not supported" % proto) + if proto == "TCP4": + try: + socket.inet_pton(socket.AF_INET, s_addr) + socket.inet_pton(socket.AF_INET, d_addr) + except OSError: + raise InvalidProxyLine(line) + elif proto == "TCP6": + try: + socket.inet_pton(socket.AF_INET6, s_addr) + socket.inet_pton(socket.AF_INET6, d_addr) + except OSError: + raise InvalidProxyLine(line) + + try: + s_port = int(bits[4]) + d_port = int(bits[5]) + except ValueError: + raise InvalidProxyLine("invalid port %s" % line) + + if not ((0 <= s_port <= 65535) and (0 <= d_port <= 65535)): + raise InvalidProxyLine("invalid port %s" % line) + + # Set data + self.proxy_protocol_info = { + "proxy_protocol": proto, + "client_addr": s_addr, + "client_port": s_port, + "proxy_addr": d_addr, + "proxy_port": d_port + } + + def parse_request_line(self, line_bytes): + bits = [bytes_to_str(bit) for bit in line_bytes.split(b" ", 2)] + if len(bits) != 3: + raise InvalidRequestLine(bytes_to_str(line_bytes)) + + # Method: RFC9110 Section 9 + self.method = bits[0] + + # nonstandard restriction, suitable for all IANA registered methods + # partially enforced in previous gunicorn versions + if not self.cfg.permit_unconventional_http_method: + if METHOD_BADCHAR_RE.search(self.method): + raise InvalidRequestMethod(self.method) + if not 3 <= len(bits[0]) <= 20: + raise InvalidRequestMethod(self.method) + # standard restriction: RFC9110 token + if not TOKEN_RE.fullmatch(self.method): + raise InvalidRequestMethod(self.method) + # nonstandard and dangerous + # methods are merely uppercase by convention, no case-insensitive treatment is intended + if self.cfg.casefold_http_method: + self.method = self.method.upper() + + # URI + self.uri = bits[1] + + # Python stdlib explicitly tells us it will not perform validation. + # https://docs.python.org/3/library/urllib.parse.html#url-parsing-security + # There are *four* `request-target` forms in rfc9112, none of them can be empty: + # 1. origin-form, which starts with a slash + # 2. absolute-form, which starts with a non-empty scheme + # 3. authority-form, (for CONNECT) which contains a colon after the host + # 4. asterisk-form, which is an asterisk (`\x2A`) + # => manually reject one always invalid URI: empty + if len(self.uri) == 0: + raise InvalidRequestLine(bytes_to_str(line_bytes)) + + try: + parts = split_request_uri(self.uri) + except ValueError: + raise InvalidRequestLine(bytes_to_str(line_bytes)) + self.path = parts.path or "" + self.query = parts.query or "" + self.fragment = parts.fragment or "" + + # Version + match = VERSION_RE.fullmatch(bits[2]) + if match is None: + raise InvalidHTTPVersion(bits[2]) + self.version = (int(match.group(1)), int(match.group(2))) + if not (1, 0) <= self.version < (2, 0): + # if ever relaxing this, carefully review Content-Encoding processing + if not self.cfg.permit_unconventional_http_version: + raise InvalidHTTPVersion(self.version) + + def set_body_reader(self): + super().set_body_reader() + if isinstance(self.body.reader, EOFReader): + self.body = Body(LengthReader(self.unreader, 0)) diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/http/parser.py b/netdeploy/lib/python3.11/site-packages/gunicorn/http/parser.py new file mode 100644 index 0000000..88da17a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/http/parser.py @@ -0,0 +1,51 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +from gunicorn.http.message import Request +from gunicorn.http.unreader import SocketUnreader, IterUnreader + + +class Parser: + + mesg_class = None + + def __init__(self, cfg, source, source_addr): + self.cfg = cfg + if hasattr(source, "recv"): + self.unreader = SocketUnreader(source) + else: + self.unreader = IterUnreader(source) + self.mesg = None + self.source_addr = source_addr + + # request counter (for keepalive connetions) + self.req_count = 0 + + def __iter__(self): + return self + + def __next__(self): + # Stop if HTTP dictates a stop. + if self.mesg and self.mesg.should_close(): + raise StopIteration() + + # Discard any unread body of the previous message + if self.mesg: + data = self.mesg.body.read(8192) + while data: + data = self.mesg.body.read(8192) + + # Parse the next request + self.req_count += 1 + self.mesg = self.mesg_class(self.cfg, self.unreader, self.source_addr, self.req_count) + if not self.mesg: + raise StopIteration() + return self.mesg + + next = __next__ + + +class RequestParser(Parser): + + mesg_class = Request diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/http/unreader.py b/netdeploy/lib/python3.11/site-packages/gunicorn/http/unreader.py new file mode 100644 index 0000000..9aadfbc --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/http/unreader.py @@ -0,0 +1,78 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import io +import os + +# Classes that can undo reading data from +# a given type of data source. + + +class Unreader: + def __init__(self): + self.buf = io.BytesIO() + + def chunk(self): + raise NotImplementedError() + + def read(self, size=None): + if size is not None and not isinstance(size, int): + raise TypeError("size parameter must be an int or long.") + + if size is not None: + if size == 0: + return b"" + if size < 0: + size = None + + self.buf.seek(0, os.SEEK_END) + + if size is None and self.buf.tell(): + ret = self.buf.getvalue() + self.buf = io.BytesIO() + return ret + if size is None: + d = self.chunk() + return d + + while self.buf.tell() < size: + chunk = self.chunk() + if not chunk: + ret = self.buf.getvalue() + self.buf = io.BytesIO() + return ret + self.buf.write(chunk) + data = self.buf.getvalue() + self.buf = io.BytesIO() + self.buf.write(data[size:]) + return data[:size] + + def unread(self, data): + self.buf.seek(0, os.SEEK_END) + self.buf.write(data) + + +class SocketUnreader(Unreader): + def __init__(self, sock, max_chunk=8192): + super().__init__() + self.sock = sock + self.mxchunk = max_chunk + + def chunk(self): + return self.sock.recv(self.mxchunk) + + +class IterUnreader(Unreader): + def __init__(self, iterable): + super().__init__() + self.iter = iter(iterable) + + def chunk(self): + if not self.iter: + return b"" + try: + return next(self.iter) + except StopIteration: + self.iter = None + return b"" diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/http/wsgi.py b/netdeploy/lib/python3.11/site-packages/gunicorn/http/wsgi.py new file mode 100644 index 0000000..419ac50 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/http/wsgi.py @@ -0,0 +1,401 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import io +import logging +import os +import re +import sys + +from gunicorn.http.message import TOKEN_RE +from gunicorn.http.errors import ConfigurationProblem, InvalidHeader, InvalidHeaderName +from gunicorn import SERVER_SOFTWARE, SERVER +from gunicorn import util + +# Send files in at most 1GB blocks as some operating systems can have problems +# with sending files in blocks over 2GB. +BLKSIZE = 0x3FFFFFFF + +# RFC9110 5.5: field-vchar = VCHAR / obs-text +# RFC4234 B.1: VCHAR = 0x21-x07E = printable ASCII +HEADER_VALUE_RE = re.compile(r'[ \t\x21-\x7e\x80-\xff]*') + +log = logging.getLogger(__name__) + + +class FileWrapper: + + def __init__(self, filelike, blksize=8192): + self.filelike = filelike + self.blksize = blksize + if hasattr(filelike, 'close'): + self.close = filelike.close + + def __getitem__(self, key): + data = self.filelike.read(self.blksize) + if data: + return data + raise IndexError + + +class WSGIErrorsWrapper(io.RawIOBase): + + def __init__(self, cfg): + # There is no public __init__ method for RawIOBase so + # we don't need to call super() in the __init__ method. + # pylint: disable=super-init-not-called + errorlog = logging.getLogger("gunicorn.error") + handlers = errorlog.handlers + self.streams = [] + + if cfg.errorlog == "-": + self.streams.append(sys.stderr) + handlers = handlers[1:] + + for h in handlers: + if hasattr(h, "stream"): + self.streams.append(h.stream) + + def write(self, data): + for stream in self.streams: + try: + stream.write(data) + except UnicodeError: + stream.write(data.encode("UTF-8")) + stream.flush() + + +def base_environ(cfg): + return { + "wsgi.errors": WSGIErrorsWrapper(cfg), + "wsgi.version": (1, 0), + "wsgi.multithread": False, + "wsgi.multiprocess": (cfg.workers > 1), + "wsgi.run_once": False, + "wsgi.file_wrapper": FileWrapper, + "wsgi.input_terminated": True, + "SERVER_SOFTWARE": SERVER_SOFTWARE, + } + + +def default_environ(req, sock, cfg): + env = base_environ(cfg) + env.update({ + "wsgi.input": req.body, + "gunicorn.socket": sock, + "REQUEST_METHOD": req.method, + "QUERY_STRING": req.query, + "RAW_URI": req.uri, + "SERVER_PROTOCOL": "HTTP/%s" % ".".join([str(v) for v in req.version]) + }) + return env + + +def proxy_environ(req): + info = req.proxy_protocol_info + + if not info: + return {} + + return { + "PROXY_PROTOCOL": info["proxy_protocol"], + "REMOTE_ADDR": info["client_addr"], + "REMOTE_PORT": str(info["client_port"]), + "PROXY_ADDR": info["proxy_addr"], + "PROXY_PORT": str(info["proxy_port"]), + } + + +def create(req, sock, client, server, cfg): + resp = Response(req, sock, cfg) + + # set initial environ + environ = default_environ(req, sock, cfg) + + # default variables + host = None + script_name = os.environ.get("SCRIPT_NAME", "") + + # add the headers to the environ + for hdr_name, hdr_value in req.headers: + if hdr_name == "EXPECT": + # handle expect + if hdr_value.lower() == "100-continue": + sock.send(b"HTTP/1.1 100 Continue\r\n\r\n") + elif hdr_name == 'HOST': + host = hdr_value + elif hdr_name == "SCRIPT_NAME": + script_name = hdr_value + elif hdr_name == "CONTENT-TYPE": + environ['CONTENT_TYPE'] = hdr_value + continue + elif hdr_name == "CONTENT-LENGTH": + environ['CONTENT_LENGTH'] = hdr_value + continue + + # do not change lightly, this is a common source of security problems + # RFC9110 Section 17.10 discourages ambiguous or incomplete mappings + key = 'HTTP_' + hdr_name.replace('-', '_') + if key in environ: + hdr_value = "%s,%s" % (environ[key], hdr_value) + environ[key] = hdr_value + + # set the url scheme + environ['wsgi.url_scheme'] = req.scheme + + # set the REMOTE_* keys in environ + # authors should be aware that REMOTE_HOST and REMOTE_ADDR + # may not qualify the remote addr: + # http://www.ietf.org/rfc/rfc3875 + if isinstance(client, str): + environ['REMOTE_ADDR'] = client + elif isinstance(client, bytes): + environ['REMOTE_ADDR'] = client.decode() + else: + environ['REMOTE_ADDR'] = client[0] + environ['REMOTE_PORT'] = str(client[1]) + + # handle the SERVER_* + # Normally only the application should use the Host header but since the + # WSGI spec doesn't support unix sockets, we are using it to create + # viable SERVER_* if possible. + if isinstance(server, str): + server = server.split(":") + if len(server) == 1: + # unix socket + if host: + server = host.split(':') + if len(server) == 1: + if req.scheme == "http": + server.append(80) + elif req.scheme == "https": + server.append(443) + else: + server.append('') + else: + # no host header given which means that we are not behind a + # proxy, so append an empty port. + server.append('') + environ['SERVER_NAME'] = server[0] + environ['SERVER_PORT'] = str(server[1]) + + # set the path and script name + path_info = req.path + if script_name: + if not path_info.startswith(script_name): + raise ConfigurationProblem( + "Request path %r does not start with SCRIPT_NAME %r" % + (path_info, script_name)) + path_info = path_info[len(script_name):] + environ['PATH_INFO'] = util.unquote_to_wsgi_str(path_info) + environ['SCRIPT_NAME'] = script_name + + # override the environ with the correct remote and server address if + # we are behind a proxy using the proxy protocol. + environ.update(proxy_environ(req)) + return resp, environ + + +class Response: + + def __init__(self, req, sock, cfg): + self.req = req + self.sock = sock + self.version = SERVER + self.status = None + self.chunked = False + self.must_close = False + self.headers = [] + self.headers_sent = False + self.response_length = None + self.sent = 0 + self.upgrade = False + self.cfg = cfg + + def force_close(self): + self.must_close = True + + def should_close(self): + if self.must_close or self.req.should_close(): + return True + if self.response_length is not None or self.chunked: + return False + if self.req.method == 'HEAD': + return False + if self.status_code < 200 or self.status_code in (204, 304): + return False + return True + + def start_response(self, status, headers, exc_info=None): + if exc_info: + try: + if self.status and self.headers_sent: + util.reraise(exc_info[0], exc_info[1], exc_info[2]) + finally: + exc_info = None + elif self.status is not None: + raise AssertionError("Response headers already set!") + + self.status = status + + # get the status code from the response here so we can use it to check + # the need for the connection header later without parsing the string + # each time. + try: + self.status_code = int(self.status.split()[0]) + except ValueError: + self.status_code = None + + self.process_headers(headers) + self.chunked = self.is_chunked() + return self.write + + def process_headers(self, headers): + for name, value in headers: + if not isinstance(name, str): + raise TypeError('%r is not a string' % name) + + if not TOKEN_RE.fullmatch(name): + raise InvalidHeaderName('%r' % name) + + if not isinstance(value, str): + raise TypeError('%r is not a string' % value) + + if not HEADER_VALUE_RE.fullmatch(value): + raise InvalidHeader('%r' % value) + + # RFC9110 5.5 + value = value.strip(" \t") + lname = name.lower() + if lname == "content-length": + self.response_length = int(value) + elif util.is_hoppish(name): + if lname == "connection": + # handle websocket + if value.lower() == "upgrade": + self.upgrade = True + elif lname == "upgrade": + if value.lower() == "websocket": + self.headers.append((name, value)) + + # ignore hopbyhop headers + continue + self.headers.append((name, value)) + + def is_chunked(self): + # Only use chunked responses when the client is + # speaking HTTP/1.1 or newer and there was + # no Content-Length header set. + if self.response_length is not None: + return False + elif self.req.version <= (1, 0): + return False + elif self.req.method == 'HEAD': + # Responses to a HEAD request MUST NOT contain a response body. + return False + elif self.status_code in (204, 304): + # Do not use chunked responses when the response is guaranteed to + # not have a response body. + return False + return True + + def default_headers(self): + # set the connection header + if self.upgrade: + connection = "upgrade" + elif self.should_close(): + connection = "close" + else: + connection = "keep-alive" + + headers = [ + "HTTP/%s.%s %s\r\n" % (self.req.version[0], + self.req.version[1], self.status), + "Server: %s\r\n" % self.version, + "Date: %s\r\n" % util.http_date(), + "Connection: %s\r\n" % connection + ] + if self.chunked: + headers.append("Transfer-Encoding: chunked\r\n") + return headers + + def send_headers(self): + if self.headers_sent: + return + tosend = self.default_headers() + tosend.extend(["%s: %s\r\n" % (k, v) for k, v in self.headers]) + + header_str = "%s\r\n" % "".join(tosend) + util.write(self.sock, util.to_bytestring(header_str, "latin-1")) + self.headers_sent = True + + def write(self, arg): + self.send_headers() + if not isinstance(arg, bytes): + raise TypeError('%r is not a byte' % arg) + arglen = len(arg) + tosend = arglen + if self.response_length is not None: + if self.sent >= self.response_length: + # Never write more than self.response_length bytes + return + + tosend = min(self.response_length - self.sent, tosend) + if tosend < arglen: + arg = arg[:tosend] + + # Sending an empty chunk signals the end of the + # response and prematurely closes the response + if self.chunked and tosend == 0: + return + + self.sent += tosend + util.write(self.sock, arg, self.chunked) + + def can_sendfile(self): + return self.cfg.sendfile is not False + + def sendfile(self, respiter): + if self.cfg.is_ssl or not self.can_sendfile(): + return False + + if not util.has_fileno(respiter.filelike): + return False + + fileno = respiter.filelike.fileno() + try: + offset = os.lseek(fileno, 0, os.SEEK_CUR) + if self.response_length is None: + filesize = os.fstat(fileno).st_size + nbytes = filesize - offset + else: + nbytes = self.response_length + except (OSError, io.UnsupportedOperation): + return False + + self.send_headers() + + if self.is_chunked(): + chunk_size = "%X\r\n" % nbytes + self.sock.sendall(chunk_size.encode('utf-8')) + if nbytes > 0: + self.sock.sendfile(respiter.filelike, offset=offset, count=nbytes) + + if self.is_chunked(): + self.sock.sendall(b"\r\n") + + os.lseek(fileno, offset, os.SEEK_SET) + + return True + + def write_file(self, respiter): + if not self.sendfile(respiter): + for item in respiter: + self.write(item) + + def close(self): + if not self.headers_sent: + self.send_headers() + if self.chunked: + util.write_chunk(self.sock, b"") diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/instrument/__init__.py b/netdeploy/lib/python3.11/site-packages/gunicorn/instrument/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/instrument/statsd.py b/netdeploy/lib/python3.11/site-packages/gunicorn/instrument/statsd.py new file mode 100644 index 0000000..7bc4e6f --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/instrument/statsd.py @@ -0,0 +1,134 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +"Bare-bones implementation of statsD's protocol, client-side" + +import logging +import socket +from re import sub + +from gunicorn.glogging import Logger + +# Instrumentation constants +METRIC_VAR = "metric" +VALUE_VAR = "value" +MTYPE_VAR = "mtype" +GAUGE_TYPE = "gauge" +COUNTER_TYPE = "counter" +HISTOGRAM_TYPE = "histogram" + + +class Statsd(Logger): + """statsD-based instrumentation, that passes as a logger + """ + def __init__(self, cfg): + Logger.__init__(self, cfg) + self.prefix = sub(r"^(.+[^.]+)\.*$", "\\g<1>.", cfg.statsd_prefix) + + if isinstance(cfg.statsd_host, str): + address_family = socket.AF_UNIX + else: + address_family = socket.AF_INET + + try: + self.sock = socket.socket(address_family, socket.SOCK_DGRAM) + self.sock.connect(cfg.statsd_host) + except Exception: + self.sock = None + + self.dogstatsd_tags = cfg.dogstatsd_tags + + # Log errors and warnings + def critical(self, msg, *args, **kwargs): + Logger.critical(self, msg, *args, **kwargs) + self.increment("gunicorn.log.critical", 1) + + def error(self, msg, *args, **kwargs): + Logger.error(self, msg, *args, **kwargs) + self.increment("gunicorn.log.error", 1) + + def warning(self, msg, *args, **kwargs): + Logger.warning(self, msg, *args, **kwargs) + self.increment("gunicorn.log.warning", 1) + + def exception(self, msg, *args, **kwargs): + Logger.exception(self, msg, *args, **kwargs) + self.increment("gunicorn.log.exception", 1) + + # Special treatment for info, the most common log level + def info(self, msg, *args, **kwargs): + self.log(logging.INFO, msg, *args, **kwargs) + + # skip the run-of-the-mill logs + def debug(self, msg, *args, **kwargs): + self.log(logging.DEBUG, msg, *args, **kwargs) + + def log(self, lvl, msg, *args, **kwargs): + """Log a given statistic if metric, value and type are present + """ + try: + extra = kwargs.get("extra", None) + if extra is not None: + metric = extra.get(METRIC_VAR, None) + value = extra.get(VALUE_VAR, None) + typ = extra.get(MTYPE_VAR, None) + if metric and value and typ: + if typ == GAUGE_TYPE: + self.gauge(metric, value) + elif typ == COUNTER_TYPE: + self.increment(metric, value) + elif typ == HISTOGRAM_TYPE: + self.histogram(metric, value) + else: + pass + + # Log to parent logger only if there is something to say + if msg: + Logger.log(self, lvl, msg, *args, **kwargs) + except Exception: + Logger.warning(self, "Failed to log to statsd", exc_info=True) + + # access logging + def access(self, resp, req, environ, request_time): + """Measure request duration + request_time is a datetime.timedelta + """ + Logger.access(self, resp, req, environ, request_time) + duration_in_ms = request_time.seconds * 1000 + float(request_time.microseconds) / 10 ** 3 + status = resp.status + if isinstance(status, bytes): + status = status.decode('utf-8') + if isinstance(status, str): + status = int(status.split(None, 1)[0]) + self.histogram("gunicorn.request.duration", duration_in_ms) + self.increment("gunicorn.requests", 1) + self.increment("gunicorn.request.status.%d" % status, 1) + + # statsD methods + # you can use those directly if you want + def gauge(self, name, value): + self._sock_send("{0}{1}:{2}|g".format(self.prefix, name, value)) + + def increment(self, name, value, sampling_rate=1.0): + self._sock_send("{0}{1}:{2}|c|@{3}".format(self.prefix, name, value, sampling_rate)) + + def decrement(self, name, value, sampling_rate=1.0): + self._sock_send("{0}{1}:-{2}|c|@{3}".format(self.prefix, name, value, sampling_rate)) + + def histogram(self, name, value): + self._sock_send("{0}{1}:{2}|ms".format(self.prefix, name, value)) + + def _sock_send(self, msg): + try: + if isinstance(msg, str): + msg = msg.encode("ascii") + + # http://docs.datadoghq.com/guides/dogstatsd/#datagram-format + if self.dogstatsd_tags: + msg = msg + b"|#" + self.dogstatsd_tags.encode('ascii') + + if self.sock: + self.sock.send(msg) + except Exception: + Logger.warning(self, "Error sending message to statsd", exc_info=True) diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/pidfile.py b/netdeploy/lib/python3.11/site-packages/gunicorn/pidfile.py new file mode 100644 index 0000000..b171f7d --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/pidfile.py @@ -0,0 +1,85 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import errno +import os +import tempfile + + +class Pidfile: + """\ + Manage a PID file. If a specific name is provided + it and '"%s.oldpid" % name' will be used. Otherwise + we create a temp file using os.mkstemp. + """ + + def __init__(self, fname): + self.fname = fname + self.pid = None + + def create(self, pid): + oldpid = self.validate() + if oldpid: + if oldpid == os.getpid(): + return + msg = "Already running on PID %s (or pid file '%s' is stale)" + raise RuntimeError(msg % (oldpid, self.fname)) + + self.pid = pid + + # Write pidfile + fdir = os.path.dirname(self.fname) + if fdir and not os.path.isdir(fdir): + raise RuntimeError("%s doesn't exist. Can't create pidfile." % fdir) + fd, fname = tempfile.mkstemp(dir=fdir) + os.write(fd, ("%s\n" % self.pid).encode('utf-8')) + if self.fname: + os.rename(fname, self.fname) + else: + self.fname = fname + os.close(fd) + + # set permissions to -rw-r--r-- + os.chmod(self.fname, 420) + + def rename(self, path): + self.unlink() + self.fname = path + self.create(self.pid) + + def unlink(self): + """ delete pidfile""" + try: + with open(self.fname) as f: + pid1 = int(f.read() or 0) + + if pid1 == self.pid: + os.unlink(self.fname) + except Exception: + pass + + def validate(self): + """ Validate pidfile and make it stale if needed""" + if not self.fname: + return + try: + with open(self.fname) as f: + try: + wpid = int(f.read()) + except ValueError: + return + + try: + os.kill(wpid, 0) + return wpid + except OSError as e: + if e.args[0] == errno.EPERM: + return wpid + if e.args[0] == errno.ESRCH: + return + raise + except OSError as e: + if e.args[0] == errno.ENOENT: + return + raise diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/reloader.py b/netdeploy/lib/python3.11/site-packages/gunicorn/reloader.py new file mode 100644 index 0000000..1c67f2a --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/reloader.py @@ -0,0 +1,131 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. +# pylint: disable=no-else-continue + +import os +import os.path +import re +import sys +import time +import threading + +COMPILED_EXT_RE = re.compile(r'py[co]$') + + +class Reloader(threading.Thread): + def __init__(self, extra_files=None, interval=1, callback=None): + super().__init__() + self.daemon = True + self._extra_files = set(extra_files or ()) + self._interval = interval + self._callback = callback + + def add_extra_file(self, filename): + self._extra_files.add(filename) + + def get_files(self): + fnames = [ + COMPILED_EXT_RE.sub('py', module.__file__) + for module in tuple(sys.modules.values()) + if getattr(module, '__file__', None) + ] + + fnames.extend(self._extra_files) + + return fnames + + def run(self): + mtimes = {} + while True: + for filename in self.get_files(): + try: + mtime = os.stat(filename).st_mtime + except OSError: + continue + old_time = mtimes.get(filename) + if old_time is None: + mtimes[filename] = mtime + continue + elif mtime > old_time: + if self._callback: + self._callback(filename) + time.sleep(self._interval) + + +has_inotify = False +if sys.platform.startswith('linux'): + try: + from inotify.adapters import Inotify + import inotify.constants + has_inotify = True + except ImportError: + pass + + +if has_inotify: + + class InotifyReloader(threading.Thread): + event_mask = (inotify.constants.IN_CREATE | inotify.constants.IN_DELETE + | inotify.constants.IN_DELETE_SELF | inotify.constants.IN_MODIFY + | inotify.constants.IN_MOVE_SELF | inotify.constants.IN_MOVED_FROM + | inotify.constants.IN_MOVED_TO) + + def __init__(self, extra_files=None, callback=None): + super().__init__() + self.daemon = True + self._callback = callback + self._dirs = set() + self._watcher = Inotify() + + for extra_file in extra_files: + self.add_extra_file(extra_file) + + def add_extra_file(self, filename): + dirname = os.path.dirname(filename) + + if dirname in self._dirs: + return + + self._watcher.add_watch(dirname, mask=self.event_mask) + self._dirs.add(dirname) + + def get_dirs(self): + fnames = [ + os.path.dirname(os.path.abspath(COMPILED_EXT_RE.sub('py', module.__file__))) + for module in tuple(sys.modules.values()) + if getattr(module, '__file__', None) + ] + + return set(fnames) + + def run(self): + self._dirs = self.get_dirs() + + for dirname in self._dirs: + if os.path.isdir(dirname): + self._watcher.add_watch(dirname, mask=self.event_mask) + + for event in self._watcher.event_gen(): + if event is None: + continue + + filename = event[3] + + self._callback(filename) + +else: + + class InotifyReloader: + def __init__(self, extra_files=None, callback=None): + raise ImportError('You must have the inotify module installed to ' + 'use the inotify reloader') + + +preferred_reloader = InotifyReloader if has_inotify else Reloader + +reloader_engines = { + 'auto': preferred_reloader, + 'poll': Reloader, + 'inotify': InotifyReloader, +} diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/sock.py b/netdeploy/lib/python3.11/site-packages/gunicorn/sock.py new file mode 100644 index 0000000..eb2b6fa --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/sock.py @@ -0,0 +1,231 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import errno +import os +import socket +import ssl +import stat +import sys +import time + +from gunicorn import util + + +class BaseSocket: + + def __init__(self, address, conf, log, fd=None): + self.log = log + self.conf = conf + + self.cfg_addr = address + if fd is None: + sock = socket.socket(self.FAMILY, socket.SOCK_STREAM) + bound = False + else: + sock = socket.fromfd(fd, self.FAMILY, socket.SOCK_STREAM) + os.close(fd) + bound = True + + self.sock = self.set_options(sock, bound=bound) + + def __str__(self): + return "" % self.sock.fileno() + + def __getattr__(self, name): + return getattr(self.sock, name) + + def set_options(self, sock, bound=False): + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + if (self.conf.reuse_port + and hasattr(socket, 'SO_REUSEPORT')): # pragma: no cover + try: + sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEPORT, 1) + except OSError as err: + if err.errno not in (errno.ENOPROTOOPT, errno.EINVAL): + raise + if not bound: + self.bind(sock) + sock.setblocking(0) + + # make sure that the socket can be inherited + if hasattr(sock, "set_inheritable"): + sock.set_inheritable(True) + + sock.listen(self.conf.backlog) + return sock + + def bind(self, sock): + sock.bind(self.cfg_addr) + + def close(self): + if self.sock is None: + return + + try: + self.sock.close() + except OSError as e: + self.log.info("Error while closing socket %s", str(e)) + + self.sock = None + + +class TCPSocket(BaseSocket): + + FAMILY = socket.AF_INET + + def __str__(self): + if self.conf.is_ssl: + scheme = "https" + else: + scheme = "http" + + addr = self.sock.getsockname() + return "%s://%s:%d" % (scheme, addr[0], addr[1]) + + def set_options(self, sock, bound=False): + sock.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) + return super().set_options(sock, bound=bound) + + +class TCP6Socket(TCPSocket): + + FAMILY = socket.AF_INET6 + + def __str__(self): + (host, port, _, _) = self.sock.getsockname() + return "http://[%s]:%d" % (host, port) + + +class UnixSocket(BaseSocket): + + FAMILY = socket.AF_UNIX + + def __init__(self, addr, conf, log, fd=None): + if fd is None: + try: + st = os.stat(addr) + except OSError as e: + if e.args[0] != errno.ENOENT: + raise + else: + if stat.S_ISSOCK(st.st_mode): + os.remove(addr) + else: + raise ValueError("%r is not a socket" % addr) + super().__init__(addr, conf, log, fd=fd) + + def __str__(self): + return "unix:%s" % self.cfg_addr + + def bind(self, sock): + old_umask = os.umask(self.conf.umask) + sock.bind(self.cfg_addr) + util.chown(self.cfg_addr, self.conf.uid, self.conf.gid) + os.umask(old_umask) + + +def _sock_type(addr): + if isinstance(addr, tuple): + if util.is_ipv6(addr[0]): + sock_type = TCP6Socket + else: + sock_type = TCPSocket + elif isinstance(addr, (str, bytes)): + sock_type = UnixSocket + else: + raise TypeError("Unable to create socket from: %r" % addr) + return sock_type + + +def create_sockets(conf, log, fds=None): + """ + Create a new socket for the configured addresses or file descriptors. + + If a configured address is a tuple then a TCP socket is created. + If it is a string, a Unix socket is created. Otherwise, a TypeError is + raised. + """ + listeners = [] + + # get it only once + addr = conf.address + fdaddr = [bind for bind in addr if isinstance(bind, int)] + if fds: + fdaddr += list(fds) + laddr = [bind for bind in addr if not isinstance(bind, int)] + + # check ssl config early to raise the error on startup + # only the certfile is needed since it can contains the keyfile + if conf.certfile and not os.path.exists(conf.certfile): + raise ValueError('certfile "%s" does not exist' % conf.certfile) + + if conf.keyfile and not os.path.exists(conf.keyfile): + raise ValueError('keyfile "%s" does not exist' % conf.keyfile) + + # sockets are already bound + if fdaddr: + for fd in fdaddr: + sock = socket.fromfd(fd, socket.AF_UNIX, socket.SOCK_STREAM) + sock_name = sock.getsockname() + sock_type = _sock_type(sock_name) + listener = sock_type(sock_name, conf, log, fd=fd) + listeners.append(listener) + + return listeners + + # no sockets is bound, first initialization of gunicorn in this env. + for addr in laddr: + sock_type = _sock_type(addr) + sock = None + for i in range(5): + try: + sock = sock_type(addr, conf, log) + except OSError as e: + if e.args[0] == errno.EADDRINUSE: + log.error("Connection in use: %s", str(addr)) + if e.args[0] == errno.EADDRNOTAVAIL: + log.error("Invalid address: %s", str(addr)) + msg = "connection to {addr} failed: {error}" + log.error(msg.format(addr=str(addr), error=str(e))) + if i < 5: + log.debug("Retrying in 1 second.") + time.sleep(1) + else: + break + + if sock is None: + log.error("Can't connect to %s", str(addr)) + sys.exit(1) + + listeners.append(sock) + + return listeners + + +def close_sockets(listeners, unlink=True): + for sock in listeners: + sock_name = sock.getsockname() + sock.close() + if unlink and _sock_type(sock_name) is UnixSocket: + os.unlink(sock_name) + + +def ssl_context(conf): + def default_ssl_context_factory(): + context = ssl.create_default_context(ssl.Purpose.CLIENT_AUTH, cafile=conf.ca_certs) + context.load_cert_chain(certfile=conf.certfile, keyfile=conf.keyfile) + context.verify_mode = conf.cert_reqs + if conf.ciphers: + context.set_ciphers(conf.ciphers) + return context + + return conf.ssl_context(conf, default_ssl_context_factory) + + +def ssl_wrap_socket(sock, conf): + return ssl_context(conf).wrap_socket(sock, + server_side=True, + suppress_ragged_eofs=conf.suppress_ragged_eofs, + do_handshake_on_connect=conf.do_handshake_on_connect) diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/systemd.py b/netdeploy/lib/python3.11/site-packages/gunicorn/systemd.py new file mode 100644 index 0000000..9b18550 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/systemd.py @@ -0,0 +1,75 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. + +import os +import socket + +SD_LISTEN_FDS_START = 3 + + +def listen_fds(unset_environment=True): + """ + Get the number of sockets inherited from systemd socket activation. + + :param unset_environment: clear systemd environment variables unless False + :type unset_environment: bool + :return: the number of sockets to inherit from systemd socket activation + :rtype: int + + Returns zero immediately if $LISTEN_PID is not set to the current pid. + Otherwise, returns the number of systemd activation sockets specified by + $LISTEN_FDS. + + When $LISTEN_PID matches the current pid, unsets the environment variables + unless the ``unset_environment`` flag is ``False``. + + .. note:: + Unlike the sd_listen_fds C function, this implementation does not set + the FD_CLOEXEC flag because the gunicorn arbiter never needs to do this. + + .. seealso:: + ``_ + + """ + fds = int(os.environ.get('LISTEN_FDS', 0)) + listen_pid = int(os.environ.get('LISTEN_PID', 0)) + + if listen_pid != os.getpid(): + return 0 + + if unset_environment: + os.environ.pop('LISTEN_PID', None) + os.environ.pop('LISTEN_FDS', None) + + return fds + + +def sd_notify(state, logger, unset_environment=False): + """Send a notification to systemd. state is a string; see + the man page of sd_notify (http://www.freedesktop.org/software/systemd/man/sd_notify.html) + for a description of the allowable values. + + If the unset_environment parameter is True, sd_notify() will unset + the $NOTIFY_SOCKET environment variable before returning (regardless of + whether the function call itself succeeded or not). Further calls to + sd_notify() will then fail, but the variable is no longer inherited by + child processes. + """ + + addr = os.environ.get('NOTIFY_SOCKET') + if addr is None: + # not run in a service, just a noop + return + try: + sock = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM | socket.SOCK_CLOEXEC) + if addr[0] == '@': + addr = '\0' + addr[1:] + sock.connect(addr) + sock.sendall(state.encode('utf-8')) + except Exception: + logger.debug("Exception while invoking sd_notify()", exc_info=True) + finally: + if unset_environment: + os.environ.pop('NOTIFY_SOCKET') + sock.close() diff --git a/netdeploy/lib/python3.11/site-packages/gunicorn/util.py b/netdeploy/lib/python3.11/site-packages/gunicorn/util.py new file mode 100644 index 0000000..ecd8174 --- /dev/null +++ b/netdeploy/lib/python3.11/site-packages/gunicorn/util.py @@ -0,0 +1,653 @@ +# +# This file is part of gunicorn released under the MIT license. +# See the NOTICE for more information. +import ast +import email.utils +import errno +import fcntl +import html +import importlib +import inspect +import io +import logging +import os +import pwd +import random +import re +import socket +import sys +import textwrap +import time +import traceback +import warnings + +try: + import importlib.metadata as importlib_metadata +except (ModuleNotFoundError, ImportError): + import importlib_metadata + +from gunicorn.errors import AppImportError +from gunicorn.workers import SUPPORTED_WORKERS +import urllib.parse + +REDIRECT_TO = getattr(os, 'devnull', '/dev/null') + +# Server and Date aren't technically hop-by-hop +# headers, but they are in the purview of the +# origin server which the WSGI spec says we should +# act like. So we drop them and add our own. +# +# In the future, concatenation server header values +# might be better, but nothing else does it and +# dropping them is easier. +hop_headers = set(""" + connection keep-alive proxy-authenticate proxy-authorization + te trailers transfer-encoding upgrade + server date + """.split()) + +try: + from setproctitle import setproctitle + + def _setproctitle(title): + setproctitle("gunicorn: %s" % title) +except ImportError: + def _setproctitle(title): + pass + + +def load_entry_point(distribution, group, name): + dist_obj = importlib_metadata.distribution(distribution) + eps = [ep for ep in dist_obj.entry_points + if ep.group == group and ep.name == name] + if not eps: + raise ImportError("Entry point %r not found" % ((group, name),)) + return eps[0].load() + + +def load_class(uri, default="gunicorn.workers.sync.SyncWorker", + section="gunicorn.workers"): + if inspect.isclass(uri): + return uri + if uri.startswith("egg:"): + # uses entry points + entry_str = uri.split("egg:")[1] + try: + dist, name = entry_str.rsplit("#", 1) + except ValueError: + dist = entry_str + name = default + + try: + return load_entry_point(dist, section, name) + except Exception: + exc = traceback.format_exc() + msg = "class uri %r invalid or not found: \n\n[%s]" + raise RuntimeError(msg % (uri, exc)) + else: + components = uri.split('.') + if len(components) == 1: + while True: + if uri.startswith("#"): + uri = uri[1:] + + if uri in SUPPORTED_WORKERS: + components = SUPPORTED_WORKERS[uri].split(".") + break + + try: + return load_entry_point( + "gunicorn", section, uri + ) + except Exception: + exc = traceback.format_exc() + msg = "class uri %r invalid or not found: \n\n[%s]" + raise RuntimeError(msg % (uri, exc)) + + klass = components.pop(-1) + + try: + mod = importlib.import_module('.'.join(components)) + except Exception: + exc = traceback.format_exc() + msg = "class uri %r invalid or not found: \n\n[%s]" + raise RuntimeError(msg % (uri, exc)) + return getattr(mod, klass) + + +positionals = ( + inspect.Parameter.POSITIONAL_ONLY, + inspect.Parameter.POSITIONAL_OR_KEYWORD, +) + + +def get_arity(f): + sig = inspect.signature(f) + arity = 0 + + for param in sig.parameters.values(): + if param.kind in positionals: + arity += 1 + + return arity + + +def get_username(uid): + """ get the username for a user id""" + return pwd.getpwuid(uid).pw_name + + +def set_owner_process(uid, gid, initgroups=False): + """ set user and group of workers processes """ + + if gid: + if uid: + try: + username = get_username(uid) + except KeyError: + initgroups = False + + # versions of python < 2.6.2 don't manage unsigned int for + # groups like on osx or fedora + gid = abs(gid) & 0x7FFFFFFF + + if initgroups: + os.initgroups(username, gid) + elif gid != os.getgid(): + os.setgid(gid) + + if uid and uid != os.getuid(): + os.setuid(uid) + + +def chown(path, uid, gid): + os.chown(path, uid, gid) + + +if sys.platform.startswith("win"): + def _waitfor(func, pathname, waitall=False): + # Perform the operation + func(pathname) + # Now setup the wait loop + if waitall: + dirname = pathname + else: + dirname, name = os.path.split(pathname) + dirname = dirname or '.' + # Check for `pathname` to be removed from the filesystem. + # The exponential backoff of the timeout amounts to a total + # of ~1 second after which the deletion is probably an error + # anyway. + # Testing on a i7@4.3GHz shows that usually only 1 iteration is + # required when contention occurs. + timeout = 0.001 + while timeout < 1.0: + # Note we are only testing for the existence of the file(s) in + # the contents of the directory regardless of any security or + # access rights. If we have made it this far, we have sufficient + # permissions to do that much using Python's equivalent of the + # Windows API FindFirstFile. + # Other Windows APIs can fail or give incorrect results when + # dealing with files that are pending deletion. + L = os.listdir(dirname) + if not L if waitall else name in L: + return + # Increase the timeout and try again + time.sleep(timeout) + timeout *= 2 + warnings.warn('tests may fail, delete still pending for ' + pathname, + RuntimeWarning, stacklevel=4) + + def _unlink(filename): + _waitfor(os.unlink, filename) +else: + _unlink = os.unlink + + +def unlink(filename): + try: + _unlink(filename) + except OSError as error: + # The filename need not exist. + if error.errno not in (errno.ENOENT, errno.ENOTDIR): + raise + + +def is_ipv6(addr): + try: + socket.inet_pton(socket.AF_INET6, addr) + except OSError: # not a valid address + return False + except ValueError: # ipv6 not supported on this platform + return False + return True + + +def parse_address(netloc, default_port='8000'): + if re.match(r'unix:(//)?', netloc): + return re.split(r'unix:(//)?', netloc)[-1] + + if netloc.startswith("fd://"): + fd = netloc[5:] + try: + return int(fd) + except ValueError: + raise RuntimeError("%r is not a valid file descriptor." % fd) from None + + if netloc.startswith("tcp://"): + netloc = netloc.split("tcp://")[1] + host, port = netloc, default_port + + if '[' in netloc and ']' in netloc: + host = netloc.split(']')[0][1:] + port = (netloc.split(']:') + [default_port])[1] + elif ':' in netloc: + host, port = (netloc.split(':') + [default_port])[:2] + elif netloc == "": + host, port = "0.0.0.0", default_port + + try: + port = int(port) + except ValueError: + raise RuntimeError("%r is not a valid port number." % port) + + return host.lower(), port + + +def close_on_exec(fd): + flags = fcntl.fcntl(fd, fcntl.F_GETFD) + flags |= fcntl.FD_CLOEXEC + fcntl.fcntl(fd, fcntl.F_SETFD, flags) + + +def set_non_blocking(fd): + flags = fcntl.fcntl(fd, fcntl.F_GETFL) | os.O_NONBLOCK + fcntl.fcntl(fd, fcntl.F_SETFL, flags) + + +def close(sock): + try: + sock.close() + except OSError: + pass + + +try: + from os import closerange +except ImportError: + def closerange(fd_low, fd_high): + # Iterate through and close all file descriptors. + for fd in range(fd_low, fd_high): + try: + os.close(fd) + except OSError: # ERROR, fd wasn't open to begin with (ignored) + pass + + +def write_chunk(sock, data): + if isinstance(data, str): + data = data.encode('utf-8') + chunk_size = "%X\r\n" % len(data) + chunk = b"".join([chunk_size.encode('utf-8'), data, b"\r\n"]) + sock.sendall(chunk) + + +def write(sock, data, chunked=False): + if chunked: + return write_chunk(sock, data) + sock.sendall(data) + + +def write_nonblock(sock, data, chunked=False): + timeout = sock.gettimeout() + if timeout != 0.0: + try: + sock.setblocking(0) + return write(sock, data, chunked) + finally: + sock.setblocking(1) + else: + return write(sock, data, chunked) + + +def write_error(sock, status_int, reason, mesg): + html_error = textwrap.dedent("""\ + + + %(reason)s + + +