From 773a91dd2fb384d27f6a4d1ada87838ea6d4ddf1 Mon Sep 17 00:00:00 2001 From: benny Date: Tue, 18 Nov 2025 20:28:52 +0000 Subject: [PATCH] Initial import of Brookhaven site --- .gitignore | 7 + app.py | 336 + history/main.html | 253 + static/United_Flyer.jpg | Bin 0 -> 494402 bytes static/buffteks.png | Bin 0 -> 182232 bytes tapdown/bin/Activate.ps1 | 247 + tapdown/bin/activate | 69 + tapdown/bin/activate.csh | 26 + tapdown/bin/activate.fish | 69 + tapdown/bin/flask | 8 + tapdown/bin/gunicorn | 8 + tapdown/bin/pip | 8 + tapdown/bin/pip3 | 8 + tapdown/bin/pip3.11 | 8 + tapdown/bin/python | 1 + tapdown/bin/python3 | 1 + tapdown/bin/python3.11 | 1 + .../site/python3.11/greenlet/greenlet.h | 164 + .../Flask_SocketIO-5.5.1.dist-info/INSTALLER | 1 + .../Flask_SocketIO-5.5.1.dist-info/LICENSE | 20 + .../Flask_SocketIO-5.5.1.dist-info/METADATA | 76 + .../Flask_SocketIO-5.5.1.dist-info/RECORD | 13 + .../Flask_SocketIO-5.5.1.dist-info/REQUESTED | 0 .../Flask_SocketIO-5.5.1.dist-info/WHEEL | 5 + .../top_level.txt | 1 + .../site-packages/_distutils_hack/__init__.py | 222 + .../site-packages/_distutils_hack/override.py | 1 + .../bidict-0.23.1.dist-info/INSTALLER | 1 + .../bidict-0.23.1.dist-info/LICENSE | 376 + .../bidict-0.23.1.dist-info/METADATA | 260 + .../bidict-0.23.1.dist-info/RECORD | 31 + .../bidict-0.23.1.dist-info/WHEEL | 5 + .../bidict-0.23.1.dist-info/top_level.txt | 1 + .../site-packages/bidict/__init__.py | 103 + .../python3.11/site-packages/bidict/_abc.py | 79 + .../python3.11/site-packages/bidict/_base.py | 556 ++ .../site-packages/bidict/_bidict.py | 194 + .../python3.11/site-packages/bidict/_dup.py | 61 + .../python3.11/site-packages/bidict/_exc.py | 36 + .../site-packages/bidict/_frozen.py | 50 + .../python3.11/site-packages/bidict/_iter.py | 51 + .../site-packages/bidict/_orderedbase.py | 238 + .../site-packages/bidict/_orderedbidict.py | 172 + .../site-packages/bidict/_typing.py | 49 + .../site-packages/bidict/metadata.py | 14 + .../python3.11/site-packages/bidict/py.typed | 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/dateutil/__init__.py | 24 + .../site-packages/dateutil/_common.py | 43 + .../site-packages/dateutil/_version.py | 4 + .../site-packages/dateutil/easter.py | 89 + .../site-packages/dateutil/parser/__init__.py | 61 + .../site-packages/dateutil/parser/_parser.py | 1613 ++++ .../dateutil/parser/isoparser.py | 416 + .../site-packages/dateutil/relativedelta.py | 599 ++ .../site-packages/dateutil/rrule.py | 1737 ++++ .../site-packages/dateutil/tz/__init__.py | 12 + .../site-packages/dateutil/tz/_common.py | 419 + .../site-packages/dateutil/tz/_factories.py | 80 + .../site-packages/dateutil/tz/tz.py | 1849 ++++ .../site-packages/dateutil/tz/win.py | 370 + .../site-packages/dateutil/tzwin.py | 2 + .../site-packages/dateutil/utils.py | 71 + .../dateutil/zoneinfo/__init__.py | 167 + .../zoneinfo/dateutil-zoneinfo.tar.gz | Bin 0 -> 156400 bytes .../dateutil/zoneinfo/rebuild.py | 75 + .../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/engineio/__init__.py | 13 + .../site-packages/engineio/async_client.py | 689 ++ .../engineio/async_drivers/__init__.py | 0 .../engineio/async_drivers/_websocket_wsgi.py | 34 + .../engineio/async_drivers/aiohttp.py | 127 + .../engineio/async_drivers/asgi.py | 296 + .../engineio/async_drivers/eventlet.py | 52 + .../engineio/async_drivers/gevent.py | 83 + .../engineio/async_drivers/gevent_uwsgi.py | 168 + .../engineio/async_drivers/sanic.py | 148 + .../engineio/async_drivers/threading.py | 19 + .../engineio/async_drivers/tornado.py | 182 + .../site-packages/engineio/async_server.py | 611 ++ .../site-packages/engineio/async_socket.py | 261 + .../site-packages/engineio/base_client.py | 169 + .../site-packages/engineio/base_server.py | 358 + .../site-packages/engineio/base_socket.py | 14 + .../site-packages/engineio/client.py | 632 ++ .../site-packages/engineio/exceptions.py | 22 + .../python3.11/site-packages/engineio/json.py | 16 + .../site-packages/engineio/middleware.py | 86 + .../site-packages/engineio/packet.py | 82 + .../site-packages/engineio/payload.py | 46 + .../site-packages/engineio/server.py | 503 + .../site-packages/engineio/socket.py | 256 + .../site-packages/engineio/static_files.py | 60 + .../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.1.2.dist-info/INSTALLER | 1 + .../flask-3.1.2.dist-info/METADATA | 91 + .../flask-3.1.2.dist-info/RECORD | 58 + .../flask-3.1.2.dist-info/REQUESTED | 0 .../site-packages/flask-3.1.2.dist-info/WHEEL | 4 + .../flask-3.1.2.dist-info/entry_points.txt | 3 + .../licenses/LICENSE.txt | 28 + .../site-packages/flask/__init__.py | 61 + .../site-packages/flask/__main__.py | 3 + .../lib/python3.11/site-packages/flask/app.py | 1536 +++ .../site-packages/flask/blueprints.py | 128 + .../lib/python3.11/site-packages/flask/cli.py | 1135 +++ .../python3.11/site-packages/flask/config.py | 367 + .../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 | 641 ++ .../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 | 792 ++ .../site-packages/flask/sessions.py | 399 + .../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 | 93 + .../python3.11/site-packages/flask/views.py | 191 + .../site-packages/flask/wrappers.py | 257 + .../site-packages/flask_socketio/__init__.py | 1125 +++ .../site-packages/flask_socketio/namespace.py | 54 + .../flask_socketio/test_client.py | 236 + .../INSTALLER | 1 + .../LICENSE.rst | 28 + .../flask_sqlalchemy-3.1.1.dist-info/METADATA | 109 + .../flask_sqlalchemy-3.1.1.dist-info/RECORD | 27 + .../REQUESTED | 0 .../flask_sqlalchemy-3.1.1.dist-info/WHEEL | 4 + .../flask_sqlalchemy/__init__.py | 26 + .../site-packages/flask_sqlalchemy/cli.py | 16 + .../flask_sqlalchemy/extension.py | 1008 ++ .../site-packages/flask_sqlalchemy/model.py | 330 + .../flask_sqlalchemy/pagination.py | 364 + .../site-packages/flask_sqlalchemy/py.typed | 0 .../site-packages/flask_sqlalchemy/query.py | 105 + .../flask_sqlalchemy/record_queries.py | 117 + .../site-packages/flask_sqlalchemy/session.py | 111 + .../site-packages/flask_sqlalchemy/table.py | 39 + .../flask_sqlalchemy/track_modifications.py | 88 + .../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 + .../h11-0.16.0.dist-info/INSTALLER | 1 + .../h11-0.16.0.dist-info/METADATA | 202 + .../site-packages/h11-0.16.0.dist-info/RECORD | 29 + .../site-packages/h11-0.16.0.dist-info/WHEEL | 5 + .../h11-0.16.0.dist-info/licenses/LICENSE.txt | 22 + .../h11-0.16.0.dist-info/top_level.txt | 1 + .../python3.11/site-packages/h11/__init__.py | 62 + .../lib/python3.11/site-packages/h11/_abnf.py | 132 + .../site-packages/h11/_connection.py | 659 ++ .../python3.11/site-packages/h11/_events.py | 369 + .../python3.11/site-packages/h11/_headers.py | 282 + .../python3.11/site-packages/h11/_readers.py | 250 + .../site-packages/h11/_receivebuffer.py | 153 + .../python3.11/site-packages/h11/_state.py | 365 + .../lib/python3.11/site-packages/h11/_util.py | 135 + .../python3.11/site-packages/h11/_version.py | 16 + .../python3.11/site-packages/h11/_writers.py | 145 + .../lib/python3.11/site-packages/h11/py.typed | 1 + .../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-23.0.1.dist-info/INSTALLER | 1 + .../pip-23.0.1.dist-info/LICENSE.txt | 20 + .../pip-23.0.1.dist-info/METADATA | 88 + .../site-packages/pip-23.0.1.dist-info/RECORD | 996 ++ .../pip-23.0.1.dist-info/REQUESTED | 0 .../site-packages/pip-23.0.1.dist-info/WHEEL | 5 + .../pip-23.0.1.dist-info/entry_points.txt | 4 + .../pip-23.0.1.dist-info/top_level.txt | 1 + .../python3.11/site-packages/pip/__init__.py | 13 + .../python3.11/site-packages/pip/__main__.py | 31 + .../site-packages/pip/__pip-runner__.py | 50 + .../site-packages/pip/_internal/__init__.py | 19 + .../site-packages/pip/_internal/build_env.py | 311 + .../site-packages/pip/_internal/cache.py | 293 + .../pip/_internal/cli/__init__.py | 4 + .../pip/_internal/cli/autocompletion.py | 171 + .../pip/_internal/cli/base_command.py | 216 + .../pip/_internal/cli/cmdoptions.py | 1055 ++ .../pip/_internal/cli/command_context.py | 27 + .../site-packages/pip/_internal/cli/main.py | 70 + .../pip/_internal/cli/main_parser.py | 134 + .../site-packages/pip/_internal/cli/parser.py | 294 + .../pip/_internal/cli/progress_bars.py | 68 + .../pip/_internal/cli/req_command.py | 502 + .../pip/_internal/cli/spinners.py | 159 + .../pip/_internal/cli/status_codes.py | 6 + .../pip/_internal/commands/__init__.py | 132 + .../pip/_internal/commands/cache.py | 223 + .../pip/_internal/commands/check.py | 53 + .../pip/_internal/commands/completion.py | 126 + .../pip/_internal/commands/configuration.py | 282 + .../pip/_internal/commands/debug.py | 199 + .../pip/_internal/commands/download.py | 149 + .../pip/_internal/commands/freeze.py | 97 + .../pip/_internal/commands/hash.py | 59 + .../pip/_internal/commands/help.py | 41 + .../pip/_internal/commands/index.py | 139 + .../pip/_internal/commands/inspect.py | 92 + .../pip/_internal/commands/install.py | 873 ++ .../pip/_internal/commands/list.py | 367 + .../pip/_internal/commands/search.py | 174 + .../pip/_internal/commands/show.py | 189 + .../pip/_internal/commands/uninstall.py | 113 + .../pip/_internal/commands/wheel.py | 203 + .../pip/_internal/configuration.py | 374 + .../pip/_internal/distributions/__init__.py | 21 + .../pip/_internal/distributions/base.py | 39 + .../pip/_internal/distributions/installed.py | 23 + .../pip/_internal/distributions/sdist.py | 150 + .../pip/_internal/distributions/wheel.py | 34 + .../site-packages/pip/_internal/exceptions.py | 747 ++ .../pip/_internal/index/__init__.py | 2 + .../pip/_internal/index/collector.py | 505 + .../pip/_internal/index/package_finder.py | 1029 ++ .../pip/_internal/index/sources.py | 224 + .../pip/_internal/locations/__init__.py | 467 + .../pip/_internal/locations/_distutils.py | 173 + .../pip/_internal/locations/_sysconfig.py | 213 + .../pip/_internal/locations/base.py | 81 + .../site-packages/pip/_internal/main.py | 12 + .../pip/_internal/metadata/__init__.py | 127 + .../pip/_internal/metadata/_json.py | 84 + .../pip/_internal/metadata/base.py | 688 ++ .../_internal/metadata/importlib/__init__.py | 4 + .../_internal/metadata/importlib/_compat.py | 55 + .../_internal/metadata/importlib/_dists.py | 224 + .../pip/_internal/metadata/importlib/_envs.py | 188 + .../pip/_internal/metadata/pkg_resources.py | 270 + .../pip/_internal/models/__init__.py | 2 + .../pip/_internal/models/candidate.py | 34 + .../pip/_internal/models/direct_url.py | 228 + .../pip/_internal/models/format_control.py | 80 + .../pip/_internal/models/index.py | 28 + .../_internal/models/installation_report.py | 53 + .../pip/_internal/models/link.py | 524 + .../pip/_internal/models/scheme.py | 31 + .../pip/_internal/models/search_scope.py | 133 + .../pip/_internal/models/selection_prefs.py | 51 + .../pip/_internal/models/target_python.py | 110 + .../pip/_internal/models/wheel.py | 92 + .../pip/_internal/network/__init__.py | 2 + .../pip/_internal/network/auth.py | 446 + .../pip/_internal/network/cache.py | 69 + .../pip/_internal/network/download.py | 186 + .../pip/_internal/network/lazy_wheel.py | 210 + .../pip/_internal/network/session.py | 518 + .../pip/_internal/network/utils.py | 96 + .../pip/_internal/network/xmlrpc.py | 60 + .../pip/_internal/operations/__init__.py | 0 .../_internal/operations/build/__init__.py | 0 .../operations/build/build_tracker.py | 124 + .../_internal/operations/build/metadata.py | 39 + .../operations/build/metadata_editable.py | 41 + .../operations/build/metadata_legacy.py | 74 + .../pip/_internal/operations/build/wheel.py | 37 + .../operations/build/wheel_editable.py | 46 + .../operations/build/wheel_legacy.py | 102 + .../pip/_internal/operations/check.py | 149 + .../pip/_internal/operations/freeze.py | 254 + .../_internal/operations/install/__init__.py | 2 + .../operations/install/editable_legacy.py | 47 + .../_internal/operations/install/legacy.py | 120 + .../pip/_internal/operations/install/wheel.py | 738 ++ .../pip/_internal/operations/prepare.py | 667 ++ .../site-packages/pip/_internal/pyproject.py | 174 + .../pip/_internal/req/__init__.py | 94 + .../pip/_internal/req/constructors.py | 501 + .../pip/_internal/req/req_file.py | 544 ++ .../pip/_internal/req/req_install.py | 946 ++ .../pip/_internal/req/req_set.py | 82 + .../pip/_internal/req/req_uninstall.py | 640 ++ .../pip/_internal/resolution/__init__.py | 0 .../pip/_internal/resolution/base.py | 20 + .../_internal/resolution/legacy/__init__.py | 0 .../_internal/resolution/legacy/resolver.py | 600 ++ .../resolution/resolvelib/__init__.py | 0 .../_internal/resolution/resolvelib/base.py | 141 + .../resolution/resolvelib/candidates.py | 556 ++ .../resolution/resolvelib/factory.py | 731 ++ .../resolution/resolvelib/found_candidates.py | 155 + .../resolution/resolvelib/provider.py | 248 + .../resolution/resolvelib/reporter.py | 68 + .../resolution/resolvelib/requirements.py | 166 + .../resolution/resolvelib/resolver.py | 296 + .../pip/_internal/self_outdated_check.py | 242 + .../pip/_internal/utils/__init__.py | 0 .../site-packages/pip/_internal/utils/_log.py | 38 + .../pip/_internal/utils/appdirs.py | 52 + .../pip/_internal/utils/compat.py | 63 + .../pip/_internal/utils/compatibility_tags.py | 165 + .../pip/_internal/utils/datetime.py | 11 + .../pip/_internal/utils/deprecation.py | 188 + .../pip/_internal/utils/direct_url_helpers.py | 87 + .../pip/_internal/utils/distutils_args.py | 43 + .../pip/_internal/utils/egg_link.py | 72 + .../pip/_internal/utils/encoding.py | 36 + .../pip/_internal/utils/entrypoints.py | 84 + .../pip/_internal/utils/filesystem.py | 153 + .../pip/_internal/utils/filetypes.py | 27 + .../pip/_internal/utils/glibc.py | 88 + .../pip/_internal/utils/hashes.py | 144 + .../_internal/utils/inject_securetransport.py | 35 + .../pip/_internal/utils/logging.py | 348 + .../site-packages/pip/_internal/utils/misc.py | 763 ++ .../pip/_internal/utils/models.py | 39 + .../pip/_internal/utils/packaging.py | 57 + .../pip/_internal/utils/setuptools_build.py | 195 + .../pip/_internal/utils/subprocess.py | 260 + .../pip/_internal/utils/temp_dir.py | 246 + .../pip/_internal/utils/unpacking.py | 257 + .../site-packages/pip/_internal/utils/urls.py | 62 + .../pip/_internal/utils/virtualenv.py | 104 + .../pip/_internal/utils/wheel.py | 136 + .../pip/_internal/vcs/__init__.py | 15 + .../site-packages/pip/_internal/vcs/bazaar.py | 112 + .../site-packages/pip/_internal/vcs/git.py | 526 + .../pip/_internal/vcs/mercurial.py | 163 + .../pip/_internal/vcs/subversion.py | 324 + .../pip/_internal/vcs/versioncontrol.py | 705 ++ .../pip/_internal/wheel_builder.py | 382 + .../site-packages/pip/_vendor/__init__.py | 120 + .../pip/_vendor/cachecontrol/__init__.py | 18 + .../pip/_vendor/cachecontrol/_cmd.py | 61 + .../pip/_vendor/cachecontrol/adapter.py | 137 + .../pip/_vendor/cachecontrol/cache.py | 65 + .../_vendor/cachecontrol/caches/__init__.py | 9 + .../_vendor/cachecontrol/caches/file_cache.py | 188 + .../cachecontrol/caches/redis_cache.py | 39 + .../pip/_vendor/cachecontrol/compat.py | 32 + .../pip/_vendor/cachecontrol/controller.py | 439 + .../pip/_vendor/cachecontrol/filewrapper.py | 111 + .../pip/_vendor/cachecontrol/heuristics.py | 139 + .../pip/_vendor/cachecontrol/serialize.py | 190 + .../pip/_vendor/cachecontrol/wrapper.py | 33 + .../pip/_vendor/certifi/__init__.py | 4 + .../pip/_vendor/certifi/__main__.py | 12 + .../pip/_vendor/certifi/cacert.pem | 4527 +++++++++ .../site-packages/pip/_vendor/certifi/core.py | 119 + .../pip/_vendor/chardet/__init__.py | 115 + .../pip/_vendor/chardet/big5freq.py | 386 + .../pip/_vendor/chardet/big5prober.py | 47 + .../pip/_vendor/chardet/chardistribution.py | 261 + .../pip/_vendor/chardet/charsetgroupprober.py | 106 + .../pip/_vendor/chardet/charsetprober.py | 147 + .../pip/_vendor/chardet/cli/__init__.py | 0 .../pip/_vendor/chardet/cli/chardetect.py | 112 + .../pip/_vendor/chardet/codingstatemachine.py | 90 + .../_vendor/chardet/codingstatemachinedict.py | 19 + .../pip/_vendor/chardet/cp949prober.py | 49 + .../pip/_vendor/chardet/enums.py | 85 + .../pip/_vendor/chardet/escprober.py | 102 + .../pip/_vendor/chardet/escsm.py | 261 + .../pip/_vendor/chardet/eucjpprober.py | 102 + .../pip/_vendor/chardet/euckrfreq.py | 196 + .../pip/_vendor/chardet/euckrprober.py | 47 + .../pip/_vendor/chardet/euctwfreq.py | 388 + .../pip/_vendor/chardet/euctwprober.py | 47 + .../pip/_vendor/chardet/gb2312freq.py | 284 + .../pip/_vendor/chardet/gb2312prober.py | 47 + .../pip/_vendor/chardet/hebrewprober.py | 316 + .../pip/_vendor/chardet/jisfreq.py | 325 + .../pip/_vendor/chardet/johabfreq.py | 2382 +++++ .../pip/_vendor/chardet/johabprober.py | 47 + .../pip/_vendor/chardet/jpcntx.py | 238 + .../pip/_vendor/chardet/langbulgarianmodel.py | 4649 +++++++++ .../pip/_vendor/chardet/langgreekmodel.py | 4397 +++++++++ .../pip/_vendor/chardet/langhebrewmodel.py | 4380 +++++++++ .../pip/_vendor/chardet/langhungarianmodel.py | 4649 +++++++++ .../pip/_vendor/chardet/langrussianmodel.py | 5725 +++++++++++ .../pip/_vendor/chardet/langthaimodel.py | 4380 +++++++++ .../pip/_vendor/chardet/langturkishmodel.py | 4380 +++++++++ .../pip/_vendor/chardet/latin1prober.py | 147 + .../pip/_vendor/chardet/macromanprober.py | 162 + .../pip/_vendor/chardet/mbcharsetprober.py | 95 + .../pip/_vendor/chardet/mbcsgroupprober.py | 57 + .../pip/_vendor/chardet/mbcssm.py | 661 ++ .../pip/_vendor/chardet/metadata/__init__.py | 0 .../pip/_vendor/chardet/metadata/languages.py | 352 + .../pip/_vendor/chardet/resultdict.py | 16 + .../pip/_vendor/chardet/sbcharsetprober.py | 162 + .../pip/_vendor/chardet/sbcsgroupprober.py | 88 + .../pip/_vendor/chardet/sjisprober.py | 105 + .../pip/_vendor/chardet/universaldetector.py | 362 + .../pip/_vendor/chardet/utf1632prober.py | 225 + .../pip/_vendor/chardet/utf8prober.py | 82 + .../pip/_vendor/chardet/version.py | 9 + .../pip/_vendor/colorama/__init__.py | 7 + .../pip/_vendor/colorama/ansi.py | 102 + .../pip/_vendor/colorama/ansitowin32.py | 277 + .../pip/_vendor/colorama/initialise.py | 121 + .../pip/_vendor/colorama/tests/__init__.py | 1 + .../pip/_vendor/colorama/tests/ansi_test.py | 76 + .../colorama/tests/ansitowin32_test.py | 294 + .../_vendor/colorama/tests/initialise_test.py | 189 + .../pip/_vendor/colorama/tests/isatty_test.py | 57 + .../pip/_vendor/colorama/tests/utils.py | 49 + .../_vendor/colorama/tests/winterm_test.py | 131 + .../pip/_vendor/colorama/win32.py | 180 + .../pip/_vendor/colorama/winterm.py | 195 + .../pip/_vendor/distlib/__init__.py | 23 + .../pip/_vendor/distlib/compat.py | 1116 +++ .../pip/_vendor/distlib/database.py | 1350 +++ .../pip/_vendor/distlib/index.py | 508 + .../pip/_vendor/distlib/locators.py | 1300 +++ .../pip/_vendor/distlib/manifest.py | 393 + .../pip/_vendor/distlib/markers.py | 152 + .../pip/_vendor/distlib/metadata.py | 1076 +++ .../pip/_vendor/distlib/resources.py | 358 + .../pip/_vendor/distlib/scripts.py | 437 + .../site-packages/pip/_vendor/distlib/util.py | 1932 ++++ .../pip/_vendor/distlib/version.py | 739 ++ .../pip/_vendor/distlib/wheel.py | 1082 +++ .../pip/_vendor/distro/__init__.py | 54 + .../pip/_vendor/distro/__main__.py | 4 + .../pip/_vendor/distro/distro.py | 1399 +++ .../pip/_vendor/idna/__init__.py | 44 + .../site-packages/pip/_vendor/idna/codec.py | 112 + .../site-packages/pip/_vendor/idna/compat.py | 13 + .../site-packages/pip/_vendor/idna/core.py | 400 + .../pip/_vendor/idna/idnadata.py | 2151 +++++ .../pip/_vendor/idna/intranges.py | 54 + .../pip/_vendor/idna/package_data.py | 2 + .../pip/_vendor/idna/uts46data.py | 8600 +++++++++++++++++ .../pip/_vendor/msgpack/__init__.py | 57 + .../pip/_vendor/msgpack/exceptions.py | 48 + .../site-packages/pip/_vendor/msgpack/ext.py | 193 + .../pip/_vendor/msgpack/fallback.py | 1010 ++ .../pip/_vendor/packaging/__about__.py | 26 + .../pip/_vendor/packaging/__init__.py | 25 + .../pip/_vendor/packaging/_manylinux.py | 301 + .../pip/_vendor/packaging/_musllinux.py | 136 + .../pip/_vendor/packaging/_structures.py | 61 + .../pip/_vendor/packaging/markers.py | 304 + .../pip/_vendor/packaging/requirements.py | 146 + .../pip/_vendor/packaging/specifiers.py | 802 ++ .../pip/_vendor/packaging/tags.py | 487 + .../pip/_vendor/packaging/utils.py | 136 + .../pip/_vendor/packaging/version.py | 504 + .../pip/_vendor/pkg_resources/__init__.py | 3296 +++++++ .../pip/_vendor/pkg_resources/py31compat.py | 23 + .../pip/_vendor/platformdirs/__init__.py | 342 + .../pip/_vendor/platformdirs/__main__.py | 46 + .../pip/_vendor/platformdirs/android.py | 120 + .../pip/_vendor/platformdirs/api.py | 156 + .../pip/_vendor/platformdirs/macos.py | 64 + .../pip/_vendor/platformdirs/unix.py | 181 + .../pip/_vendor/platformdirs/version.py | 4 + .../pip/_vendor/platformdirs/windows.py | 184 + .../pip/_vendor/pygments/__init__.py | 82 + .../pip/_vendor/pygments/__main__.py | 17 + .../pip/_vendor/pygments/cmdline.py | 668 ++ .../pip/_vendor/pygments/console.py | 70 + .../pip/_vendor/pygments/filter.py | 71 + .../pip/_vendor/pygments/filters/__init__.py | 940 ++ .../pip/_vendor/pygments/formatter.py | 94 + .../_vendor/pygments/formatters/__init__.py | 143 + .../_vendor/pygments/formatters/_mapping.py | 23 + .../pip/_vendor/pygments/formatters/bbcode.py | 108 + .../pip/_vendor/pygments/formatters/groff.py | 170 + .../pip/_vendor/pygments/formatters/html.py | 989 ++ .../pip/_vendor/pygments/formatters/img.py | 645 ++ .../pip/_vendor/pygments/formatters/irc.py | 179 + .../pip/_vendor/pygments/formatters/latex.py | 521 + .../pip/_vendor/pygments/formatters/other.py | 161 + .../pygments/formatters/pangomarkup.py | 83 + .../pip/_vendor/pygments/formatters/rtf.py | 146 + .../pip/_vendor/pygments/formatters/svg.py | 188 + .../_vendor/pygments/formatters/terminal.py | 127 + .../pygments/formatters/terminal256.py | 338 + .../pip/_vendor/pygments/lexer.py | 882 ++ .../pip/_vendor/pygments/lexers/__init__.py | 335 + .../pip/_vendor/pygments/lexers/_mapping.py | 541 ++ .../pip/_vendor/pygments/lexers/python.py | 1204 +++ .../pip/_vendor/pygments/modeline.py | 43 + .../pip/_vendor/pygments/plugin.py | 88 + .../pip/_vendor/pygments/regexopt.py | 91 + .../pip/_vendor/pygments/scanner.py | 104 + .../pip/_vendor/pygments/sphinxext.py | 155 + .../pip/_vendor/pygments/style.py | 197 + .../pip/_vendor/pygments/styles/__init__.py | 97 + .../pip/_vendor/pygments/token.py | 213 + .../pip/_vendor/pygments/unistring.py | 153 + .../pip/_vendor/pygments/util.py | 308 + .../pip/_vendor/pyparsing/__init__.py | 331 + .../pip/_vendor/pyparsing/actions.py | 207 + .../pip/_vendor/pyparsing/common.py | 424 + .../pip/_vendor/pyparsing/core.py | 5814 +++++++++++ .../pip/_vendor/pyparsing/diagram/__init__.py | 642 ++ .../pip/_vendor/pyparsing/exceptions.py | 267 + .../pip/_vendor/pyparsing/helpers.py | 1088 +++ .../pip/_vendor/pyparsing/results.py | 760 ++ .../pip/_vendor/pyparsing/testing.py | 331 + .../pip/_vendor/pyparsing/unicode.py | 352 + .../pip/_vendor/pyparsing/util.py | 235 + .../pip/_vendor/pyproject_hooks/__init__.py | 23 + .../pip/_vendor/pyproject_hooks/_compat.py | 8 + .../pip/_vendor/pyproject_hooks/_impl.py | 330 + .../pyproject_hooks/_in_process/__init__.py | 18 + .../_in_process/_in_process.py | 353 + .../pip/_vendor/requests/__init__.py | 182 + .../pip/_vendor/requests/__version__.py | 14 + .../pip/_vendor/requests/_internal_utils.py | 48 + .../pip/_vendor/requests/adapters.py | 584 ++ .../site-packages/pip/_vendor/requests/api.py | 157 + .../pip/_vendor/requests/auth.py | 315 + .../pip/_vendor/requests/certs.py | 24 + .../pip/_vendor/requests/compat.py | 67 + .../pip/_vendor/requests/cookies.py | 561 ++ .../pip/_vendor/requests/exceptions.py | 141 + .../pip/_vendor/requests/help.py | 131 + .../pip/_vendor/requests/hooks.py | 33 + .../pip/_vendor/requests/models.py | 1034 ++ .../pip/_vendor/requests/packages.py | 16 + .../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 | 26 + .../pip/_vendor/resolvelib/compat/__init__.py | 0 .../resolvelib/compat/collections_abc.py | 6 + .../pip/_vendor/resolvelib/providers.py | 133 + .../pip/_vendor/resolvelib/reporters.py | 43 + .../pip/_vendor/resolvelib/resolvers.py | 482 + .../pip/_vendor/resolvelib/structs.py | 165 + .../pip/_vendor/rich/__init__.py | 177 + .../pip/_vendor/rich/__main__.py | 274 + .../pip/_vendor/rich/_cell_widths.py | 451 + .../pip/_vendor/rich/_emoji_codes.py | 3610 +++++++ .../pip/_vendor/rich/_emoji_replace.py | 32 + .../pip/_vendor/rich/_export_format.py | 78 + .../pip/_vendor/rich/_extension.py | 10 + .../pip/_vendor/rich/_inspect.py | 270 + .../pip/_vendor/rich/_log_render.py | 94 + .../site-packages/pip/_vendor/rich/_loop.py | 43 + .../pip/_vendor/rich/_null_file.py | 83 + .../pip/_vendor/rich/_palettes.py | 309 + .../site-packages/pip/_vendor/rich/_pick.py | 17 + .../site-packages/pip/_vendor/rich/_ratio.py | 160 + .../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 | 662 ++ .../pip/_vendor/rich/_windows.py | 72 + .../pip/_vendor/rich/_windows_renderer.py | 56 + .../site-packages/pip/_vendor/rich/_wrap.py | 56 + .../site-packages/pip/_vendor/rich/abc.py | 33 + .../site-packages/pip/_vendor/rich/align.py | 311 + .../site-packages/pip/_vendor/rich/ansi.py | 237 + .../site-packages/pip/_vendor/rich/bar.py | 94 + .../site-packages/pip/_vendor/rich/box.py | 517 + .../site-packages/pip/_vendor/rich/cells.py | 154 + .../site-packages/pip/_vendor/rich/color.py | 618 ++ .../pip/_vendor/rich/color_triplet.py | 38 + .../site-packages/pip/_vendor/rich/columns.py | 187 + .../site-packages/pip/_vendor/rich/console.py | 2612 +++++ .../pip/_vendor/rich/constrain.py | 37 + .../pip/_vendor/rich/containers.py | 167 + .../site-packages/pip/_vendor/rich/control.py | 225 + .../pip/_vendor/rich/default_styles.py | 188 + .../pip/_vendor/rich/diagnose.py | 37 + .../site-packages/pip/_vendor/rich/emoji.py | 96 + .../site-packages/pip/_vendor/rich/errors.py | 34 + .../pip/_vendor/rich/file_proxy.py | 54 + .../pip/_vendor/rich/filesize.py | 89 + .../pip/_vendor/rich/highlighter.py | 232 + .../site-packages/pip/_vendor/rich/json.py | 140 + .../site-packages/pip/_vendor/rich/jupyter.py | 101 + .../site-packages/pip/_vendor/rich/layout.py | 443 + .../site-packages/pip/_vendor/rich/live.py | 373 + .../pip/_vendor/rich/live_render.py | 113 + .../site-packages/pip/_vendor/rich/logging.py | 289 + .../site-packages/pip/_vendor/rich/markup.py | 246 + .../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 | 308 + .../site-packages/pip/_vendor/rich/pretty.py | 1029 ++ .../pip/_vendor/rich/progress.py | 1707 ++++ .../pip/_vendor/rich/progress_bar.py | 224 + .../site-packages/pip/_vendor/rich/prompt.py | 376 + .../pip/_vendor/rich/protocol.py | 42 + .../site-packages/pip/_vendor/rich/region.py | 10 + .../site-packages/pip/_vendor/rich/repr.py | 149 + .../site-packages/pip/_vendor/rich/rule.py | 134 + .../site-packages/pip/_vendor/rich/scope.py | 86 + .../site-packages/pip/_vendor/rich/screen.py | 54 + .../site-packages/pip/_vendor/rich/segment.py | 739 ++ .../site-packages/pip/_vendor/rich/spinner.py | 136 + .../site-packages/pip/_vendor/rich/status.py | 132 + .../site-packages/pip/_vendor/rich/style.py | 773 ++ .../site-packages/pip/_vendor/rich/styled.py | 42 + .../site-packages/pip/_vendor/rich/syntax.py | 945 ++ .../site-packages/pip/_vendor/rich/table.py | 1002 ++ .../pip/_vendor/rich/terminal_theme.py | 153 + .../site-packages/pip/_vendor/rich/text.py | 1311 +++ .../site-packages/pip/_vendor/rich/theme.py | 112 + .../site-packages/pip/_vendor/rich/themes.py | 5 + .../pip/_vendor/rich/traceback.py | 677 ++ .../site-packages/pip/_vendor/rich/tree.py | 251 + .../site-packages/pip/_vendor/six.py | 998 ++ .../pip/_vendor/tenacity/__init__.py | 519 + .../pip/_vendor/tenacity/_asyncio.py | 92 + .../pip/_vendor/tenacity/_utils.py | 68 + .../pip/_vendor/tenacity/after.py | 46 + .../pip/_vendor/tenacity/before.py | 41 + .../pip/_vendor/tenacity/before_sleep.py | 58 + .../site-packages/pip/_vendor/tenacity/nap.py | 43 + .../pip/_vendor/tenacity/retry.py | 240 + .../pip/_vendor/tenacity/stop.py | 96 + .../pip/_vendor/tenacity/tornadoweb.py | 59 + .../pip/_vendor/tenacity/wait.py | 232 + .../pip/_vendor/tomli/__init__.py | 11 + .../pip/_vendor/tomli/_parser.py | 691 ++ .../site-packages/pip/_vendor/tomli/_re.py | 107 + .../site-packages/pip/_vendor/tomli/_types.py | 10 + .../pip/_vendor/typing_extensions.py | 2209 +++++ .../pip/_vendor/urllib3/__init__.py | 102 + .../pip/_vendor/urllib3/_collections.py | 337 + .../pip/_vendor/urllib3/_version.py | 2 + .../pip/_vendor/urllib3/connection.py | 567 ++ .../pip/_vendor/urllib3/connectionpool.py | 1110 +++ .../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 | 921 ++ .../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 + .../pip/_vendor/urllib3/packages/six.py | 1076 +++ .../pip/_vendor/urllib3/poolmanager.py | 537 + .../pip/_vendor/urllib3/request.py | 170 + .../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 | 620 ++ .../pip/_vendor/urllib3/util/ssl_.py | 495 + .../urllib3/util/ssl_match_hostname.py | 159 + .../pip/_vendor/urllib3/util/ssltransport.py | 221 + .../pip/_vendor/urllib3/util/timeout.py | 268 + .../pip/_vendor/urllib3/util/url.py | 435 + .../pip/_vendor/urllib3/util/wait.py | 152 + .../site-packages/pip/_vendor/vendor.txt | 23 + .../pip/_vendor/webencodings/__init__.py | 342 + .../pip/_vendor/webencodings/labels.py | 231 + .../pip/_vendor/webencodings/mklabels.py | 59 + .../pip/_vendor/webencodings/tests.py | 153 + .../_vendor/webencodings/x_user_defined.py | 325 + .../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 + .../pymysql-1.1.2.dist-info/INSTALLER | 1 + .../pymysql-1.1.2.dist-info/METADATA | 131 + .../pymysql-1.1.2.dist-info/RECORD | 43 + .../pymysql-1.1.2.dist-info/REQUESTED | 0 .../pymysql-1.1.2.dist-info/WHEEL | 5 + .../pymysql-1.1.2.dist-info/licenses/LICENSE | 19 + .../pymysql-1.1.2.dist-info/top_level.txt | 1 + .../site-packages/pymysql/__init__.py | 183 + .../python3.11/site-packages/pymysql/_auth.py | 272 + .../site-packages/pymysql/charset.py | 217 + .../site-packages/pymysql/connections.py | 1435 +++ .../site-packages/pymysql/constants/CLIENT.py | 38 + .../pymysql/constants/COMMAND.py | 32 + .../site-packages/pymysql/constants/CR.py | 79 + .../site-packages/pymysql/constants/ER.py | 477 + .../pymysql/constants/FIELD_TYPE.py | 31 + .../site-packages/pymysql/constants/FLAG.py | 15 + .../pymysql/constants/SERVER_STATUS.py | 10 + .../pymysql/constants/__init__.py | 0 .../site-packages/pymysql/converters.py | 363 + .../site-packages/pymysql/cursors.py | 531 + .../python3.11/site-packages/pymysql/err.py | 150 + .../site-packages/pymysql/optionfile.py | 21 + .../site-packages/pymysql/protocol.py | 356 + .../python3.11/site-packages/pymysql/times.py | 20 + .../INSTALLER | 1 + .../LICENSE | 54 + .../METADATA | 204 + .../RECORD | 45 + .../REQUESTED | 0 .../WHEEL | 6 + .../top_level.txt | 1 + .../zip-safe | 1 + .../INSTALLER | 1 + .../python_engineio-4.12.3.dist-info/METADATA | 49 + .../python_engineio-4.12.3.dist-info/RECORD | 58 + .../python_engineio-4.12.3.dist-info/WHEEL | 5 + .../licenses/LICENSE | 20 + .../top_level.txt | 1 + .../INSTALLER | 1 + .../python_socketio-5.14.1.dist-info/METADATA | 72 + .../python_socketio-5.14.1.dist-info/RECORD | 68 + .../python_socketio-5.14.1.dist-info/WHEEL | 5 + .../licenses/LICENSE | 20 + .../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 + .../INSTALLER | 1 + .../simple_websocket-1.1.0.dist-info/LICENSE | 21 + .../simple_websocket-1.1.0.dist-info/METADATA | 37 + .../simple_websocket-1.1.0.dist-info/RECORD | 16 + .../simple_websocket-1.1.0.dist-info/WHEEL | 5 + .../top_level.txt | 1 + .../simple_websocket/__init__.py | 3 + .../site-packages/simple_websocket/aiows.py | 467 + .../site-packages/simple_websocket/asgi.py | 50 + .../site-packages/simple_websocket/errors.py | 20 + .../site-packages/simple_websocket/ws.py | 488 + .../six-1.17.0.dist-info/INSTALLER | 1 + .../six-1.17.0.dist-info/LICENSE | 18 + .../six-1.17.0.dist-info/METADATA | 43 + .../site-packages/six-1.17.0.dist-info/RECORD | 8 + .../site-packages/six-1.17.0.dist-info/WHEEL | 6 + .../six-1.17.0.dist-info/top_level.txt | 1 + tapdown/lib/python3.11/site-packages/six.py | 1003 ++ .../site-packages/socketio/__init__.py | 28 + .../site-packages/socketio/admin.py | 391 + .../python3.11/site-packages/socketio/asgi.py | 47 + .../site-packages/socketio/async_admin.py | 384 + .../socketio/async_aiopika_manager.py | 126 + .../site-packages/socketio/async_client.py | 608 ++ .../site-packages/socketio/async_manager.py | 120 + .../site-packages/socketio/async_namespace.py | 287 + .../socketio/async_pubsub_manager.py | 236 + .../socketio/async_redis_manager.py | 164 + .../site-packages/socketio/async_server.py | 714 ++ .../socketio/async_simple_client.py | 214 + .../site-packages/socketio/base_client.py | 296 + .../site-packages/socketio/base_manager.py | 161 + .../site-packages/socketio/base_namespace.py | 33 + .../site-packages/socketio/base_server.py | 266 + .../site-packages/socketio/client.py | 559 ++ .../site-packages/socketio/exceptions.py | 38 + .../site-packages/socketio/kafka_manager.py | 65 + .../site-packages/socketio/kombu_manager.py | 134 + .../site-packages/socketio/manager.py | 93 + .../site-packages/socketio/middleware.py | 40 + .../site-packages/socketio/msgpack_packet.py | 18 + .../site-packages/socketio/namespace.py | 212 + .../site-packages/socketio/packet.py | 190 + .../site-packages/socketio/pubsub_manager.py | 226 + .../site-packages/socketio/redis_manager.py | 197 + .../site-packages/socketio/server.py | 676 ++ .../site-packages/socketio/simple_client.py | 196 + .../site-packages/socketio/tornado.py | 9 + .../site-packages/socketio/zmq_manager.py | 105 + .../sqlalchemy-2.0.43.dist-info/INSTALLER | 1 + .../sqlalchemy-2.0.43.dist-info/METADATA | 243 + .../sqlalchemy-2.0.43.dist-info/RECORD | 531 + .../sqlalchemy-2.0.43.dist-info/WHEEL | 6 + .../licenses/LICENSE | 19 + .../sqlalchemy-2.0.43.dist-info/top_level.txt | 1 + .../site-packages/sqlalchemy/__init__.py | 283 + .../sqlalchemy/connectors/__init__.py | 18 + .../sqlalchemy/connectors/aioodbc.py | 184 + .../sqlalchemy/connectors/asyncio.py | 351 + .../sqlalchemy/connectors/pyodbc.py | 250 + .../sqlalchemy/cyextension/__init__.py | 6 + ...ollections.cpython-311-x86_64-linux-gnu.so | Bin 0 -> 2164440 bytes .../sqlalchemy/cyextension/collections.pyx | 409 + ...utabledict.cpython-311-x86_64-linux-gnu.so | Bin 0 -> 727040 bytes .../sqlalchemy/cyextension/immutabledict.pxd | 8 + .../sqlalchemy/cyextension/immutabledict.pyx | 133 + ...processors.cpython-311-x86_64-linux-gnu.so | Bin 0 -> 589952 bytes .../sqlalchemy/cyextension/processors.pyx | 68 + ...esultproxy.cpython-311-x86_64-linux-gnu.so | Bin 0 -> 595576 bytes .../sqlalchemy/cyextension/resultproxy.pyx | 102 + .../util.cpython-311-x86_64-linux-gnu.so | Bin 0 -> 902584 bytes .../sqlalchemy/cyextension/util.pyx | 90 + .../sqlalchemy/dialects/__init__.py | 62 + .../sqlalchemy/dialects/_typing.py | 30 + .../sqlalchemy/dialects/mssql/__init__.py | 88 + .../sqlalchemy/dialects/mssql/aioodbc.py | 63 + .../sqlalchemy/dialects/mssql/base.py | 4084 ++++++++ .../dialects/mssql/information_schema.py | 285 + .../sqlalchemy/dialects/mssql/json.py | 129 + .../sqlalchemy/dialects/mssql/provision.py | 162 + .../sqlalchemy/dialects/mssql/pymssql.py | 126 + .../sqlalchemy/dialects/mssql/pyodbc.py | 760 ++ .../sqlalchemy/dialects/mysql/__init__.py | 104 + .../sqlalchemy/dialects/mysql/aiomysql.py | 244 + .../sqlalchemy/dialects/mysql/asyncmy.py | 225 + .../sqlalchemy/dialects/mysql/base.py | 3923 ++++++++ .../sqlalchemy/dialects/mysql/cymysql.py | 106 + .../sqlalchemy/dialects/mysql/dml.py | 225 + .../sqlalchemy/dialects/mysql/enumerated.py | 282 + .../sqlalchemy/dialects/mysql/expression.py | 146 + .../sqlalchemy/dialects/mysql/json.py | 91 + .../sqlalchemy/dialects/mysql/mariadb.py | 73 + .../dialects/mysql/mariadbconnector.py | 322 + .../dialects/mysql/mysqlconnector.py | 302 + .../sqlalchemy/dialects/mysql/mysqldb.py | 314 + .../sqlalchemy/dialects/mysql/provision.py | 113 + .../sqlalchemy/dialects/mysql/pymysql.py | 158 + .../sqlalchemy/dialects/mysql/pyodbc.py | 157 + .../sqlalchemy/dialects/mysql/reflection.py | 727 ++ .../dialects/mysql/reserved_words.py | 570 ++ .../sqlalchemy/dialects/mysql/types.py | 835 ++ .../sqlalchemy/dialects/oracle/__init__.py | 81 + .../sqlalchemy/dialects/oracle/base.py | 3802 ++++++++ .../sqlalchemy/dialects/oracle/cx_oracle.py | 1555 +++ .../sqlalchemy/dialects/oracle/dictionary.py | 507 + .../sqlalchemy/dialects/oracle/oracledb.py | 947 ++ .../sqlalchemy/dialects/oracle/provision.py | 220 + .../sqlalchemy/dialects/oracle/types.py | 316 + .../sqlalchemy/dialects/oracle/vector.py | 364 + .../dialects/postgresql/__init__.py | 167 + .../dialects/postgresql/_psycopg_common.py | 189 + .../sqlalchemy/dialects/postgresql/array.py | 509 + .../sqlalchemy/dialects/postgresql/asyncpg.py | 1293 +++ .../sqlalchemy/dialects/postgresql/base.py | 5226 ++++++++++ .../sqlalchemy/dialects/postgresql/dml.py | 339 + .../sqlalchemy/dialects/postgresql/ext.py | 536 + .../sqlalchemy/dialects/postgresql/hstore.py | 406 + .../sqlalchemy/dialects/postgresql/json.py | 367 + .../dialects/postgresql/named_types.py | 524 + .../dialects/postgresql/operators.py | 129 + .../sqlalchemy/dialects/postgresql/pg8000.py | 669 ++ .../dialects/postgresql/pg_catalog.py | 326 + .../dialects/postgresql/provision.py | 175 + .../sqlalchemy/dialects/postgresql/psycopg.py | 783 ++ .../dialects/postgresql/psycopg2.py | 892 ++ .../dialects/postgresql/psycopg2cffi.py | 61 + .../sqlalchemy/dialects/postgresql/ranges.py | 1031 ++ .../sqlalchemy/dialects/postgresql/types.py | 313 + .../sqlalchemy/dialects/sqlite/__init__.py | 57 + .../sqlalchemy/dialects/sqlite/aiosqlite.py | 443 + .../sqlalchemy/dialects/sqlite/base.py | 2953 ++++++ .../sqlalchemy/dialects/sqlite/dml.py | 263 + .../sqlalchemy/dialects/sqlite/json.py | 92 + .../sqlalchemy/dialects/sqlite/provision.py | 196 + .../sqlalchemy/dialects/sqlite/pysqlcipher.py | 157 + .../sqlalchemy/dialects/sqlite/pysqlite.py | 726 ++ .../dialects/type_migration_guidelines.txt | 145 + .../sqlalchemy/engine/__init__.py | 62 + .../sqlalchemy/engine/_py_processors.py | 136 + .../sqlalchemy/engine/_py_row.py | 128 + .../sqlalchemy/engine/_py_util.py | 74 + .../site-packages/sqlalchemy/engine/base.py | 3374 +++++++ .../sqlalchemy/engine/characteristics.py | 155 + .../site-packages/sqlalchemy/engine/create.py | 893 ++ .../site-packages/sqlalchemy/engine/cursor.py | 2181 +++++ .../sqlalchemy/engine/default.py | 2389 +++++ .../site-packages/sqlalchemy/engine/events.py | 965 ++ .../sqlalchemy/engine/interfaces.py | 3464 +++++++ .../site-packages/sqlalchemy/engine/mock.py | 134 + .../sqlalchemy/engine/processors.py | 61 + .../sqlalchemy/engine/reflection.py | 2102 ++++ .../site-packages/sqlalchemy/engine/result.py | 2387 +++++ .../site-packages/sqlalchemy/engine/row.py | 400 + .../sqlalchemy/engine/strategies.py | 16 + .../site-packages/sqlalchemy/engine/url.py | 924 ++ .../site-packages/sqlalchemy/engine/util.py | 167 + .../sqlalchemy/event/__init__.py | 25 + .../site-packages/sqlalchemy/event/api.py | 220 + .../site-packages/sqlalchemy/event/attr.py | 655 ++ .../site-packages/sqlalchemy/event/base.py | 472 + .../site-packages/sqlalchemy/event/legacy.py | 246 + .../sqlalchemy/event/registry.py | 390 + .../site-packages/sqlalchemy/events.py | 17 + .../site-packages/sqlalchemy/exc.py | 832 ++ .../site-packages/sqlalchemy/ext/__init__.py | 11 + .../sqlalchemy/ext/associationproxy.py | 2027 ++++ .../sqlalchemy/ext/asyncio/__init__.py | 25 + .../sqlalchemy/ext/asyncio/base.py | 281 + .../sqlalchemy/ext/asyncio/engine.py | 1469 +++ .../sqlalchemy/ext/asyncio/exc.py | 21 + .../sqlalchemy/ext/asyncio/result.py | 962 ++ .../sqlalchemy/ext/asyncio/scoping.py | 1613 ++++ .../sqlalchemy/ext/asyncio/session.py | 1961 ++++ .../site-packages/sqlalchemy/ext/automap.py | 1701 ++++ .../site-packages/sqlalchemy/ext/baked.py | 570 ++ .../site-packages/sqlalchemy/ext/compiler.py | 600 ++ .../sqlalchemy/ext/declarative/__init__.py | 65 + .../sqlalchemy/ext/declarative/extensions.py | 564 ++ .../sqlalchemy/ext/horizontal_shard.py | 478 + .../site-packages/sqlalchemy/ext/hybrid.py | 1533 +++ .../site-packages/sqlalchemy/ext/indexable.py | 346 + .../sqlalchemy/ext/instrumentation.py | 450 + .../site-packages/sqlalchemy/ext/mutable.py | 1096 +++ .../sqlalchemy/ext/mypy/__init__.py | 6 + .../sqlalchemy/ext/mypy/apply.py | 324 + .../sqlalchemy/ext/mypy/decl_class.py | 515 + .../sqlalchemy/ext/mypy/infer.py | 590 ++ .../sqlalchemy/ext/mypy/names.py | 335 + .../sqlalchemy/ext/mypy/plugin.py | 303 + .../site-packages/sqlalchemy/ext/mypy/util.py | 357 + .../sqlalchemy/ext/orderinglist.py | 439 + .../sqlalchemy/ext/serializer.py | 185 + .../sqlalchemy/future/__init__.py | 16 + .../site-packages/sqlalchemy/future/engine.py | 15 + .../site-packages/sqlalchemy/inspection.py | 174 + .../site-packages/sqlalchemy/log.py | 288 + .../site-packages/sqlalchemy/orm/__init__.py | 170 + .../sqlalchemy/orm/_orm_constructors.py | 2661 +++++ .../site-packages/sqlalchemy/orm/_typing.py | 179 + .../sqlalchemy/orm/attributes.py | 2845 ++++++ .../site-packages/sqlalchemy/orm/base.py | 971 ++ .../sqlalchemy/orm/bulk_persistence.py | 2135 ++++ .../sqlalchemy/orm/clsregistry.py | 571 ++ .../sqlalchemy/orm/collections.py | 1627 ++++ .../site-packages/sqlalchemy/orm/context.py | 3334 +++++++ .../site-packages/sqlalchemy/orm/decl_api.py | 1920 ++++ .../site-packages/sqlalchemy/orm/decl_base.py | 2192 +++++ .../sqlalchemy/orm/dependency.py | 1302 +++ .../sqlalchemy/orm/descriptor_props.py | 1092 +++ .../site-packages/sqlalchemy/orm/dynamic.py | 300 + .../site-packages/sqlalchemy/orm/evaluator.py | 379 + .../site-packages/sqlalchemy/orm/events.py | 3269 +++++++ .../site-packages/sqlalchemy/orm/exc.py | 237 + .../site-packages/sqlalchemy/orm/identity.py | 302 + .../sqlalchemy/orm/instrumentation.py | 754 ++ .../sqlalchemy/orm/interfaces.py | 1496 +++ .../site-packages/sqlalchemy/orm/loading.py | 1686 ++++ .../sqlalchemy/orm/mapped_collection.py | 557 ++ .../site-packages/sqlalchemy/orm/mapper.py | 4435 +++++++++ .../sqlalchemy/orm/path_registry.py | 809 ++ .../sqlalchemy/orm/persistence.py | 1788 ++++ .../sqlalchemy/orm/properties.py | 907 ++ .../site-packages/sqlalchemy/orm/query.py | 3453 +++++++ .../sqlalchemy/orm/relationships.py | 3508 +++++++ .../site-packages/sqlalchemy/orm/scoping.py | 2162 +++++ .../site-packages/sqlalchemy/orm/session.py | 5294 ++++++++++ .../site-packages/sqlalchemy/orm/state.py | 1143 +++ .../sqlalchemy/orm/state_changes.py | 196 + .../sqlalchemy/orm/strategies.py | 3470 +++++++ .../sqlalchemy/orm/strategy_options.py | 2568 +++++ .../site-packages/sqlalchemy/orm/sync.py | 164 + .../sqlalchemy/orm/unitofwork.py | 796 ++ .../site-packages/sqlalchemy/orm/util.py | 2403 +++++ .../site-packages/sqlalchemy/orm/writeonly.py | 674 ++ .../site-packages/sqlalchemy/pool/__init__.py | 44 + .../site-packages/sqlalchemy/pool/base.py | 1516 +++ .../site-packages/sqlalchemy/pool/events.py | 372 + .../site-packages/sqlalchemy/pool/impl.py | 579 ++ .../site-packages/sqlalchemy/py.typed | 0 .../site-packages/sqlalchemy/schema.py | 69 + .../site-packages/sqlalchemy/sql/__init__.py | 145 + .../sqlalchemy/sql/_dml_constructors.py | 132 + .../sqlalchemy/sql/_elements_constructors.py | 1872 ++++ .../sqlalchemy/sql/_orm_types.py | 20 + .../site-packages/sqlalchemy/sql/_py_util.py | 75 + .../sql/_selectable_constructors.py | 763 ++ .../site-packages/sqlalchemy/sql/_typing.py | 468 + .../sqlalchemy/sql/annotation.py | 585 ++ .../site-packages/sqlalchemy/sql/base.py | 2219 +++++ .../site-packages/sqlalchemy/sql/cache_key.py | 1057 ++ .../site-packages/sqlalchemy/sql/coercions.py | 1403 +++ .../site-packages/sqlalchemy/sql/compiler.py | 7999 +++++++++++++++ .../site-packages/sqlalchemy/sql/crud.py | 1744 ++++ .../site-packages/sqlalchemy/sql/ddl.py | 1444 +++ .../sqlalchemy/sql/default_comparator.py | 551 ++ .../site-packages/sqlalchemy/sql/dml.py | 1837 ++++ .../site-packages/sqlalchemy/sql/elements.py | 5553 +++++++++++ .../site-packages/sqlalchemy/sql/events.py | 458 + .../sqlalchemy/sql/expression.py | 159 + .../site-packages/sqlalchemy/sql/functions.py | 2104 ++++ .../site-packages/sqlalchemy/sql/lambdas.py | 1442 +++ .../site-packages/sqlalchemy/sql/naming.py | 209 + .../site-packages/sqlalchemy/sql/operators.py | 2623 +++++ .../site-packages/sqlalchemy/sql/roles.py | 323 + .../site-packages/sqlalchemy/sql/schema.py | 6218 ++++++++++++ .../sqlalchemy/sql/selectable.py | 7219 ++++++++++++++ .../site-packages/sqlalchemy/sql/sqltypes.py | 3930 ++++++++ .../sqlalchemy/sql/traversals.py | 1024 ++ .../site-packages/sqlalchemy/sql/type_api.py | 2368 +++++ .../site-packages/sqlalchemy/sql/util.py | 1485 +++ .../site-packages/sqlalchemy/sql/visitors.py | 1164 +++ .../sqlalchemy/testing/__init__.py | 96 + .../sqlalchemy/testing/assertions.py | 991 ++ .../sqlalchemy/testing/assertsql.py | 516 + .../sqlalchemy/testing/asyncio.py | 135 + .../sqlalchemy/testing/config.py | 423 + .../sqlalchemy/testing/engines.py | 474 + .../sqlalchemy/testing/entities.py | 117 + .../sqlalchemy/testing/exclusions.py | 435 + .../sqlalchemy/testing/fixtures/__init__.py | 28 + .../sqlalchemy/testing/fixtures/base.py | 371 + .../sqlalchemy/testing/fixtures/mypy.py | 332 + .../sqlalchemy/testing/fixtures/orm.py | 227 + .../sqlalchemy/testing/fixtures/sql.py | 503 + .../sqlalchemy/testing/pickleable.py | 155 + .../sqlalchemy/testing/plugin/__init__.py | 6 + .../sqlalchemy/testing/plugin/bootstrap.py | 51 + .../sqlalchemy/testing/plugin/plugin_base.py | 779 ++ .../sqlalchemy/testing/plugin/pytestplugin.py | 867 ++ .../sqlalchemy/testing/profiling.py | 324 + .../sqlalchemy/testing/provision.py | 502 + .../sqlalchemy/testing/requirements.py | 1918 ++++ .../sqlalchemy/testing/schema.py | 224 + .../sqlalchemy/testing/suite/__init__.py | 19 + .../sqlalchemy/testing/suite/test_cte.py | 237 + .../sqlalchemy/testing/suite/test_ddl.py | 389 + .../testing/suite/test_deprecations.py | 153 + .../sqlalchemy/testing/suite/test_dialect.py | 776 ++ .../sqlalchemy/testing/suite/test_insert.py | 630 ++ .../testing/suite/test_reflection.py | 3370 +++++++ .../sqlalchemy/testing/suite/test_results.py | 504 + .../sqlalchemy/testing/suite/test_rowcount.py | 258 + .../sqlalchemy/testing/suite/test_select.py | 2008 ++++ .../sqlalchemy/testing/suite/test_sequence.py | 317 + .../sqlalchemy/testing/suite/test_types.py | 2145 ++++ .../testing/suite/test_unicode_ddl.py | 189 + .../testing/suite/test_update_delete.py | 139 + .../site-packages/sqlalchemy/testing/util.py | 535 + .../sqlalchemy/testing/warnings.py | 52 + .../site-packages/sqlalchemy/types.py | 74 + .../site-packages/sqlalchemy/util/__init__.py | 160 + .../sqlalchemy/util/_collections.py | 717 ++ .../sqlalchemy/util/_concurrency_py3k.py | 288 + .../site-packages/sqlalchemy/util/_has_cy.py | 40 + .../sqlalchemy/util/_py_collections.py | 541 ++ .../site-packages/sqlalchemy/util/compat.py | 303 + .../sqlalchemy/util/concurrency.py | 108 + .../sqlalchemy/util/deprecations.py | 401 + .../sqlalchemy/util/langhelpers.py | 2303 +++++ .../sqlalchemy/util/preloaded.py | 150 + .../site-packages/sqlalchemy/util/queue.py | 322 + .../sqlalchemy/util/tool_support.py | 201 + .../sqlalchemy/util/topological.py | 120 + .../site-packages/sqlalchemy/util/typing.py | 733 ++ .../INSTALLER | 1 + .../METADATA | 72 + .../typing_extensions-4.15.0.dist-info/RECORD | 7 + .../typing_extensions-4.15.0.dist-info/WHEEL | 4 + .../licenses/LICENSE | 279 + .../site-packages/typing_extensions.py | 4317 +++++++++ .../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 ++ .../wsproto-1.2.0.dist-info/INSTALLER | 1 + .../wsproto-1.2.0.dist-info/LICENSE | 21 + .../wsproto-1.2.0.dist-info/METADATA | 177 + .../wsproto-1.2.0.dist-info/RECORD | 23 + .../wsproto-1.2.0.dist-info/WHEEL | 5 + .../wsproto-1.2.0.dist-info/top_level.txt | 1 + .../site-packages/wsproto/__init__.py | 94 + .../site-packages/wsproto/connection.py | 189 + .../site-packages/wsproto/events.py | 295 + .../site-packages/wsproto/extensions.py | 315 + .../site-packages/wsproto/frame_protocol.py | 673 ++ .../site-packages/wsproto/handshake.py | 491 + .../python3.11/site-packages/wsproto/py.typed | 1 + .../site-packages/wsproto/typing.py | 3 + .../site-packages/wsproto/utilities.py | 88 + tapdown/lib64 | 1 + tapdown/pyvenv.cfg | 5 + templates/about.html | 30 + templates/admin_inquiries.html | 45 + templates/base.html | 153 + templates/contact.html | 84 + templates/form.html | 134 + templates/host.html | 49 + templates/host_lobby.html | 76 + templates/host_responses.html | 154 + templates/host_room.html | 53 + templates/index.html | 36 + templates/play.html | 35 + templates/play_ai.html | 196 + templates/queue.html | 27 + templates/services.html | 23 + templates/survey.html | 36 + templates/thanks.html | 10 + templates/work.html | 22 + wsgi.py | 7 + 1900 files changed, 700543 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 history/main.html create mode 100644 static/United_Flyer.jpg create mode 100644 static/buffteks.png create mode 100644 tapdown/bin/Activate.ps1 create mode 100644 tapdown/bin/activate create mode 100644 tapdown/bin/activate.csh create mode 100644 tapdown/bin/activate.fish create mode 100755 tapdown/bin/flask create mode 100755 tapdown/bin/gunicorn create mode 100755 tapdown/bin/pip create mode 100755 tapdown/bin/pip3 create mode 100755 tapdown/bin/pip3.11 create mode 120000 tapdown/bin/python create mode 120000 tapdown/bin/python3 create mode 120000 tapdown/bin/python3.11 create mode 100644 tapdown/include/site/python3.11/greenlet/greenlet.h create mode 100644 tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/REQUESTED create mode 100644 tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/WHEEL create mode 100755 tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/_distutils_hack/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/_distutils_hack/override.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/bidict/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/_abc.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/_base.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/_bidict.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/_dup.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/_exc.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/_frozen.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/_iter.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/_orderedbase.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/_orderedbidict.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/_typing.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/metadata.py create mode 100644 tapdown/lib/python3.11/site-packages/bidict/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/LICENSE.txt create mode 100644 tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/blinker/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/blinker/_utilities.py create mode 100644 tapdown/lib/python3.11/site-packages/blinker/base.py create mode 100644 tapdown/lib/python3.11/site-packages/blinker/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/licenses/LICENSE.txt create mode 100644 tapdown/lib/python3.11/site-packages/click/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/click/_compat.py create mode 100644 tapdown/lib/python3.11/site-packages/click/_termui_impl.py create mode 100644 tapdown/lib/python3.11/site-packages/click/_textwrap.py create mode 100644 tapdown/lib/python3.11/site-packages/click/_utils.py create mode 100644 tapdown/lib/python3.11/site-packages/click/_winconsole.py create mode 100644 tapdown/lib/python3.11/site-packages/click/core.py create mode 100644 tapdown/lib/python3.11/site-packages/click/decorators.py create mode 100644 tapdown/lib/python3.11/site-packages/click/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/click/formatting.py create mode 100644 tapdown/lib/python3.11/site-packages/click/globals.py create mode 100644 tapdown/lib/python3.11/site-packages/click/parser.py create mode 100644 tapdown/lib/python3.11/site-packages/click/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/click/shell_completion.py create mode 100644 tapdown/lib/python3.11/site-packages/click/termui.py create mode 100644 tapdown/lib/python3.11/site-packages/click/testing.py create mode 100644 tapdown/lib/python3.11/site-packages/click/types.py create mode 100644 tapdown/lib/python3.11/site-packages/click/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/_common.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/_version.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/easter.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/parser/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/parser/_parser.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/parser/isoparser.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/relativedelta.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/rrule.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/tz/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/tz/_common.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/tz/_factories.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/tz/tz.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/tz/win.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/tzwin.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz create mode 100644 tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/rebuild.py create mode 100644 tapdown/lib/python3.11/site-packages/distutils-precedence.pth create mode 100644 tapdown/lib/python3.11/site-packages/dns/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/_asyncbackend.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/_asyncio_backend.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/_ddr.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/_features.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/_immutable_ctx.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/_no_ssl.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/_tls_util.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/_trio_backend.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/asyncbackend.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/asyncquery.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/asyncresolver.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/btree.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/btreezone.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/dnssec.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/dnssecalgs/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/dnssecalgs/base.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/dnssecalgs/cryptography.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/dnssecalgs/dsa.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/dnssecalgs/ecdsa.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/dnssecalgs/eddsa.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/dnssecalgs/rsa.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/dnssectypes.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/e164.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/edns.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/entropy.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/enum.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/exception.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/flags.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/grange.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/immutable.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/inet.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/ipv4.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/ipv6.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/message.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/name.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/namedict.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/nameserver.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/node.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/opcode.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/dns/query.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/quic/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/quic/_asyncio.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/quic/_common.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/quic/_sync.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/quic/_trio.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rcode.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdata.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdataclass.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdataset.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdatatype.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/AFSDB.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/AMTRELAY.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/AVC.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CAA.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CDNSKEY.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CDS.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CERT.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CNAME.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CSYNC.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DLV.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DNAME.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DNSKEY.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DS.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DSYNC.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI48.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI64.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/GPOS.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/HINFO.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/HIP.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/ISDN.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/L32.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/L64.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/LOC.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/LP.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/MX.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NID.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NINFO.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NS.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/OPT.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/PTR.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RESINFO.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RP.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RRSIG.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RT.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SMIMEA.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SOA.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SPF.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SSHFP.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TKEY.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TLSA.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TSIG.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TXT.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/URI.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/WALLET.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/X25.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/ZONEMD.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/CH/A.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/CH/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/A.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/AAAA.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/APL.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/DHCID.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/HTTPS.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/IPSECKEY.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/KX.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/NAPTR.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP_PTR.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/PX.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/SRV.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/SVCB.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/WKS.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/dnskeybase.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/dsbase.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/euibase.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/mxbase.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/nsbase.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/svcbbase.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/tlsabase.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/txtbase.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rdtypes/util.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/renderer.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/resolver.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/reversename.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/rrset.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/serial.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/set.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/tokenizer.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/transaction.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/tsig.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/tsigkeyring.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/ttl.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/update.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/version.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/versioned.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/win32util.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/wire.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/xfr.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/zone.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/zonefile.py create mode 100644 tapdown/lib/python3.11/site-packages/dns/zonetypes.py create mode 100644 tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/licenses/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/engineio/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_client.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_drivers/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_drivers/_websocket_wsgi.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_drivers/aiohttp.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_drivers/asgi.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_drivers/eventlet.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_drivers/gevent.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_drivers/gevent_uwsgi.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_drivers/sanic.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_drivers/threading.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_drivers/tornado.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_server.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/async_socket.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/base_client.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/base_server.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/base_socket.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/client.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/json.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/middleware.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/packet.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/payload.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/server.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/socket.py create mode 100644 tapdown/lib/python3.11/site-packages/engineio/static_files.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/REQUESTED create mode 100644 tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/AUTHORS create mode 100644 tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/_version.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/asyncio.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/backdoor.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/convenience.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/corolocal.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/coros.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/dagpool.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/db_pool.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/debug.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/event.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/BaseHTTPServer.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/CGIHTTPServer.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/MySQLdb.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/SSL.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/crypto.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/tsafe.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/version.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/Queue.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/SimpleHTTPServer.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/SocketServer.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/_socket_nodns.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/asynchat.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/asyncore.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/builtin.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/ftplib.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/http/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/http/client.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/http/cookiejar.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/http/cookies.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/http/server.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/httplib.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/os.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/profile.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/select.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/selectors.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/socket.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/ssl.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/subprocess.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/thread.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/threading.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/time.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/urllib/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/urllib/error.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/urllib/parse.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/urllib/request.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/urllib/response.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/urllib2.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/green/zmq.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/greenio/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/greenio/base.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/greenio/py3.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/greenpool.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/greenthread.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/hubs/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/hubs/asyncio.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/hubs/epolls.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/hubs/hub.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/hubs/kqueue.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/hubs/poll.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/hubs/pyevent.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/hubs/selects.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/hubs/timer.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/lock.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/patcher.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/pools.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/queue.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/semaphore.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/support/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/support/greendns.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/support/greenlets.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/support/psycopg2_patcher.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/support/pylib.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/support/stacklesspypys.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/support/stacklesss.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/timeout.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/tpool.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/websocket.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/wsgi.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/README.rst create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/README.rst create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore.thrift create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/constants.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/ttypes.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/api.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/client.py create mode 100755 tapdown/lib/python3.11/site-packages/eventlet/zipkin/example/ex1.png create mode 100755 tapdown/lib/python3.11/site-packages/eventlet/zipkin/example/ex2.png create mode 100755 tapdown/lib/python3.11/site-packages/eventlet/zipkin/example/ex3.png create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/greenthread.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/http.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/log.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/patcher.py create mode 100644 tapdown/lib/python3.11/site-packages/eventlet/zipkin/wsgi.py create mode 100644 tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/REQUESTED create mode 100644 tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/entry_points.txt create mode 100644 tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/licenses/LICENSE.txt create mode 100644 tapdown/lib/python3.11/site-packages/flask/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/__main__.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/app.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/blueprints.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/cli.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/config.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/ctx.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/debughelpers.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/globals.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/helpers.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/json/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/json/provider.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/json/tag.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/logging.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/flask/sansio/README.md create mode 100644 tapdown/lib/python3.11/site-packages/flask/sansio/app.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/sansio/blueprints.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/sansio/scaffold.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/sessions.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/signals.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/templating.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/testing.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/typing.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/views.py create mode 100644 tapdown/lib/python3.11/site-packages/flask/wrappers.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_socketio/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_socketio/namespace.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_socketio/test_client.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/LICENSE.rst create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/REQUESTED create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/cli.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/extension.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/model.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/query.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/record_queries.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/session.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/table.py create mode 100644 tapdown/lib/python3.11/site-packages/flask_sqlalchemy/track_modifications.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE.PSF create mode 100644 tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/CObjects.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/PyGreenlet.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/PyGreenlet.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/PyGreenletUnswitchable.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/PyModule.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TBrokenGreenlet.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TExceptionState.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TGreenlet.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TGreenlet.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TGreenletGlobals.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TMainGreenlet.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TPythonState.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TStackState.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TThreadState.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TThreadStateCreator.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TThreadStateDestroy.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/TUserGreenlet.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/__init__.py create mode 100755 tapdown/lib/python3.11/site-packages/greenlet/_greenlet.cpython-311-x86_64-linux-gnu.so create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet.cpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet_allocator.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet_compiler_compat.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet_cpython_compat.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet_exceptions.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet_internal.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet_msvc_compat.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet_refs.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet_slp_switch.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/greenlet_thread_support.hpp create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/setup_switch_x64_masm.cmd create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_aarch64_gcc.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_alpha_unix.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_amd64_unix.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm32_gcc.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm32_ios.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.asm create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.obj create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm64_msvc.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_csky_gcc.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_loongarch64_linux.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_m68k_gcc.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_mips_unix.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_aix.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_linux.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_aix.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_linux.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_macosx.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_unix.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_riscv_unix.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_s390_unix.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_sh_gcc.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_sparc_sun_gcc.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x32_unix.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.asm create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.obj create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x64_msvc.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x86_msvc.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x86_unix.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/slp_platformselect.h create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/_test_extension.c create mode 100755 tapdown/lib/python3.11/site-packages/greenlet/tests/_test_extension.cpython-311-x86_64-linux-gnu.so create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpp create mode 100755 tapdown/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpython-311-x86_64-linux-gnu.so create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/fail_clearing_run_switches.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/fail_cpp_exception.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/fail_initialstub_already_started.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/fail_slp_switch.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets2.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/fail_switch_two_greenlets.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/leakcheck.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_contextvars.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_cpp.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_extension_interface.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_gc.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_generator.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_generator_nested.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_greenlet.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_greenlet_trash.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_leaks.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_stack_saved.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_throw.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_tracing.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_version.py create mode 100644 tapdown/lib/python3.11/site-packages/greenlet/tests/test_weakref.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/REQUESTED create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/entry_points.txt create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/__main__.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/app/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/app/base.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/app/pasterapp.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/app/wsgiapp.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/arbiter.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/config.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/debug.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/errors.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/glogging.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/http/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/http/body.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/http/errors.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/http/message.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/http/parser.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/http/unreader.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/http/wsgi.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/instrument/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/instrument/statsd.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/pidfile.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/reloader.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/sock.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/systemd.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/util.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/workers/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/workers/base.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/workers/base_async.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/workers/geventlet.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/workers/ggevent.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/workers/gthread.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/workers/gtornado.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/workers/sync.py create mode 100644 tapdown/lib/python3.11/site-packages/gunicorn/workers/workertmp.py create mode 100644 tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/licenses/LICENSE.txt create mode 100644 tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/h11/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/_abnf.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/_connection.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/_events.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/_headers.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/_readers.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/_receivebuffer.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/_state.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/_util.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/_version.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/_writers.py create mode 100644 tapdown/lib/python3.11/site-packages/h11/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous/_json.py create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous/encoding.py create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous/exc.py create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous/serializer.py create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous/signer.py create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous/timed.py create mode 100644 tapdown/lib/python3.11/site-packages/itsdangerous/url_safe.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/entry_points.txt create mode 100644 tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/_identifier.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/async_utils.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/bccache.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/compiler.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/constants.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/debug.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/defaults.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/environment.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/ext.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/filters.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/idtracking.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/lexer.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/loaders.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/meta.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/nativetypes.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/nodes.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/optimizer.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/parser.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/runtime.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/sandbox.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/tests.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/jinja2/visitor.py create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/licenses/LICENSE.txt create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe-3.0.3.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe/_native.py create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe/_speedups.c create mode 100755 tapdown/lib/python3.11/site-packages/markupsafe/_speedups.cpython-311-x86_64-linux-gnu.so create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe/_speedups.pyi create mode 100644 tapdown/lib/python3.11/site-packages/markupsafe/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/packaging-25.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/packaging-25.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/packaging-25.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/packaging-25.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/packaging-25.0.dist-info/licenses/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/packaging-25.0.dist-info/licenses/LICENSE.APACHE create mode 100644 tapdown/lib/python3.11/site-packages/packaging-25.0.dist-info/licenses/LICENSE.BSD create mode 100644 tapdown/lib/python3.11/site-packages/packaging/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/_elffile.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/_manylinux.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/_musllinux.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/_parser.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/_structures.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/_tokenizer.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/licenses/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/licenses/_spdx.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/markers.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/metadata.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/packaging/requirements.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/specifiers.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/tags.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/packaging/version.py create mode 100644 tapdown/lib/python3.11/site-packages/pip-23.0.1.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/pip-23.0.1.dist-info/LICENSE.txt create mode 100644 tapdown/lib/python3.11/site-packages/pip-23.0.1.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/pip-23.0.1.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/pip-23.0.1.dist-info/REQUESTED create mode 100644 tapdown/lib/python3.11/site-packages/pip-23.0.1.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/pip-23.0.1.dist-info/entry_points.txt create mode 100644 tapdown/lib/python3.11/site-packages/pip-23.0.1.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/pip/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/__main__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/__pip-runner__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/build_env.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cache.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/autocompletion.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/base_command.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/cmdoptions.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/command_context.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/main.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/main_parser.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/parser.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/progress_bars.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/req_command.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/spinners.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/cli/status_codes.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/cache.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/check.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/completion.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/configuration.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/debug.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/download.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/freeze.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/hash.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/help.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/index.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/inspect.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/install.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/list.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/search.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/show.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/uninstall.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/commands/wheel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/configuration.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/distributions/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/distributions/base.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/distributions/installed.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/distributions/sdist.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/distributions/wheel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/index/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/index/collector.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/index/package_finder.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/index/sources.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/locations/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/locations/_distutils.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/locations/_sysconfig.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/locations/base.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/main.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/metadata/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/metadata/_json.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/metadata/base.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/metadata/importlib/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_compat.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_dists.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/metadata/importlib/_envs.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/metadata/pkg_resources.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/candidate.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/direct_url.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/format_control.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/index.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/installation_report.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/link.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/scheme.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/search_scope.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/selection_prefs.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/target_python.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/models/wheel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/network/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/network/auth.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/network/cache.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/network/download.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/network/lazy_wheel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/network/session.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/network/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/network/xmlrpc.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/build/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/build/build_tracker.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/build/metadata.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/build/metadata_editable.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/build/metadata_legacy.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/build/wheel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/build/wheel_editable.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/build/wheel_legacy.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/check.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/freeze.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/install/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/install/editable_legacy.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/install/legacy.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/install/wheel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/operations/prepare.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/pyproject.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/req/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/req/constructors.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/req/req_file.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/req/req_install.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/req/req_set.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/req/req_uninstall.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/base.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/legacy/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/legacy/resolver.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/base.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/candidates.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/factory.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/found_candidates.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/provider.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/reporter.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/requirements.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/resolution/resolvelib/resolver.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/self_outdated_check.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/_log.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/appdirs.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/compat.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/compatibility_tags.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/datetime.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/deprecation.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/direct_url_helpers.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/distutils_args.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/egg_link.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/encoding.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/entrypoints.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/filesystem.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/filetypes.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/glibc.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/hashes.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/inject_securetransport.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/logging.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/misc.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/models.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/packaging.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/setuptools_build.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/subprocess.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/temp_dir.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/unpacking.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/urls.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/virtualenv.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/utils/wheel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/vcs/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/vcs/bazaar.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/vcs/git.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/vcs/mercurial.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/vcs/subversion.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/vcs/versioncontrol.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_internal/wheel_builder.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/_cmd.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/adapter.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/cache.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/caches/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/caches/file_cache.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/caches/redis_cache.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/compat.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/controller.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/filewrapper.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/heuristics.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/serialize.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/cachecontrol/wrapper.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/certifi/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/certifi/__main__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/certifi/cacert.pem create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/certifi/core.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/big5freq.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/big5prober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/chardistribution.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/charsetgroupprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/charsetprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/cli/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/cli/chardetect.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/codingstatemachine.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/codingstatemachinedict.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/cp949prober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/enums.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/escprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/escsm.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/eucjpprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/euckrfreq.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/euckrprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/euctwfreq.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/euctwprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/gb2312freq.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/gb2312prober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/hebrewprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/jisfreq.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/johabfreq.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/johabprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/jpcntx.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/langbulgarianmodel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/langgreekmodel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/langhebrewmodel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/langhungarianmodel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/langrussianmodel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/langthaimodel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/langturkishmodel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/latin1prober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/macromanprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/mbcharsetprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/mbcsgroupprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/mbcssm.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/metadata/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/metadata/languages.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/resultdict.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/sbcharsetprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/sbcsgroupprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/sjisprober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/universaldetector.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/utf1632prober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/utf8prober.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/chardet/version.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/ansi.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/ansitowin32.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/initialise.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/tests/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/tests/ansi_test.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/tests/ansitowin32_test.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/tests/initialise_test.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/tests/isatty_test.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/tests/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/tests/winterm_test.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/win32.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/colorama/winterm.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/compat.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/database.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/index.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/locators.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/manifest.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/markers.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/metadata.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/resources.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/scripts.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/util.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/version.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distlib/wheel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distro/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distro/__main__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/distro/distro.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/idna/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/idna/codec.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/idna/compat.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/idna/core.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/idna/idnadata.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/idna/intranges.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/idna/package_data.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/idna/uts46data.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/msgpack/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/msgpack/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/msgpack/ext.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/msgpack/fallback.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/__about__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/_manylinux.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/_musllinux.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/_structures.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/markers.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/requirements.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/specifiers.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/tags.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/packaging/version.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pkg_resources/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pkg_resources/py31compat.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/platformdirs/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/platformdirs/__main__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/platformdirs/android.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/platformdirs/api.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/platformdirs/macos.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/platformdirs/unix.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/platformdirs/version.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/platformdirs/windows.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/__main__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/cmdline.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/console.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/filter.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/filters/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatter.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/_mapping.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/bbcode.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/groff.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/html.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/img.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/irc.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/latex.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/other.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/pangomarkup.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/rtf.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/svg.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/terminal.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/formatters/terminal256.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/lexer.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/lexers/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/lexers/_mapping.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/lexers/python.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/modeline.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/plugin.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/regexopt.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/scanner.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/sphinxext.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/style.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/styles/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/token.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/unistring.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pygments/util.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/actions.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/common.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/core.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/diagram/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/helpers.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/results.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/testing.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/unicode.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyparsing/util.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_compat.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_impl.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/pyproject_hooks/_in_process/_in_process.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/__version__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/_internal_utils.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/adapters.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/api.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/auth.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/certs.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/compat.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/cookies.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/help.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/hooks.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/models.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/packages.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/sessions.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/status_codes.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/structures.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/requests/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/resolvelib/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/resolvelib/compat/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/resolvelib/compat/collections_abc.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/resolvelib/providers.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/resolvelib/reporters.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/resolvelib/resolvers.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/resolvelib/structs.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/__main__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_cell_widths.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_emoji_codes.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_emoji_replace.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_export_format.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_extension.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_inspect.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_log_render.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_loop.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_null_file.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_palettes.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_pick.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_ratio.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_spinners.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_stack.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_timer.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_win32_console.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_windows.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_windows_renderer.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/_wrap.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/abc.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/align.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/ansi.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/bar.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/box.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/cells.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/color.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/color_triplet.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/columns.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/console.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/constrain.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/containers.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/control.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/default_styles.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/diagnose.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/emoji.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/errors.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/file_proxy.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/filesize.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/highlighter.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/json.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/jupyter.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/layout.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/live.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/live_render.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/logging.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/markup.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/measure.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/padding.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/pager.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/palette.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/panel.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/pretty.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/progress.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/progress_bar.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/prompt.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/protocol.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/region.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/repr.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/rule.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/scope.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/screen.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/segment.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/spinner.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/status.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/style.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/styled.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/syntax.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/table.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/terminal_theme.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/text.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/theme.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/themes.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/traceback.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/rich/tree.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/six.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/_asyncio.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/_utils.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/after.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/before.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/before_sleep.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/nap.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/retry.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/stop.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/tornadoweb.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tenacity/wait.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tomli/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tomli/_parser.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tomli/_re.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/tomli/_types.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/typing_extensions.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/_collections.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/_version.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/connection.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/connectionpool.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/_appengine_environ.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/_securetransport/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/_securetransport/bindings.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/_securetransport/low_level.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/appengine.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/ntlmpool.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/pyopenssl.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/securetransport.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/contrib/socks.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/fields.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/filepost.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/backports/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/backports/makefile.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/packages/six.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/poolmanager.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/request.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/response.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/connection.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/proxy.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/queue.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/request.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/response.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/retry.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/ssl_.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/ssl_match_hostname.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/ssltransport.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/timeout.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/url.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/urllib3/util/wait.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/vendor.txt create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/webencodings/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/webencodings/labels.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/webencodings/mklabels.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/webencodings/tests.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/_vendor/webencodings/x_user_defined.py create mode 100644 tapdown/lib/python3.11/site-packages/pip/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/_adapters.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/_common.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/_compat.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/_itertools.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/_legacy.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/abc.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/readers.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/importlib_resources/simple.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/jaraco/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/jaraco/context.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/jaraco/functools.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/jaraco/text/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/more_itertools/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/more_itertools/more.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/more_itertools/recipes.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/__about__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/_manylinux.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/_musllinux.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/_structures.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/markers.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/requirements.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/specifiers.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/tags.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/packaging/version.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/__main__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/android.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/api.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/macos.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/unix.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/version.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/platformdirs/windows.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/actions.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/common.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/core.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/diagram/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/helpers.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/results.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/testing.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/unicode.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/pyparsing/util.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/typing_extensions.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/_vendor/zipp.py create mode 100644 tapdown/lib/python3.11/site-packages/pkg_resources/extern/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql-1.1.2.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/pymysql-1.1.2.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/pymysql-1.1.2.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/pymysql-1.1.2.dist-info/REQUESTED create mode 100644 tapdown/lib/python3.11/site-packages/pymysql-1.1.2.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/pymysql-1.1.2.dist-info/licenses/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/pymysql-1.1.2.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/_auth.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/charset.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/connections.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/constants/CLIENT.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/constants/COMMAND.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/constants/CR.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/constants/ER.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/constants/FIELD_TYPE.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/constants/FLAG.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/constants/SERVER_STATUS.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/constants/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/converters.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/cursors.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/err.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/optionfile.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/protocol.py create mode 100644 tapdown/lib/python3.11/site-packages/pymysql/times.py create mode 100644 tapdown/lib/python3.11/site-packages/python_dateutil-2.9.0.post0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/python_dateutil-2.9.0.post0.dist-info/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/python_dateutil-2.9.0.post0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/python_dateutil-2.9.0.post0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/python_dateutil-2.9.0.post0.dist-info/REQUESTED create mode 100644 tapdown/lib/python3.11/site-packages/python_dateutil-2.9.0.post0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/python_dateutil-2.9.0.post0.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/python_dateutil-2.9.0.post0.dist-info/zip-safe create mode 100644 tapdown/lib/python3.11/site-packages/python_engineio-4.12.3.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/python_engineio-4.12.3.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/python_engineio-4.12.3.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/python_engineio-4.12.3.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/python_engineio-4.12.3.dist-info/licenses/LICENSE create mode 100755 tapdown/lib/python3.11/site-packages/python_engineio-4.12.3.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/python_socketio-5.14.1.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/python_socketio-5.14.1.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/python_socketio-5.14.1.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/python_socketio-5.14.1.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/python_socketio-5.14.1.dist-info/licenses/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/python_socketio-5.14.1.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/REQUESTED create mode 100644 tapdown/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/entry_points.txt create mode 100644 tapdown/lib/python3.11/site-packages/setuptools-66.1.1.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_deprecation_warning.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/_collections.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/_functools.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/_log.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/_macos_compat.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/_msvccompiler.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/archive_util.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/bcppcompiler.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/ccompiler.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/cmd.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/_framework_compat.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/bdist.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/bdist_dumb.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/bdist_rpm.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/build.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/build_clib.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/build_ext.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/build_py.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/build_scripts.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/check.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/clean.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/config.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/install.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/install_data.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/install_egg_info.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/install_headers.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/install_lib.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/install_scripts.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/py37compat.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/register.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/sdist.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/command/upload.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/config.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/core.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/cygwinccompiler.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/debug.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/dep_util.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/dir_util.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/dist.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/errors.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/extension.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/fancy_getopt.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/file_util.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/filelist.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/log.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/msvc9compiler.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/msvccompiler.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/py38compat.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/py39compat.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/spawn.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/sysconfig.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/text_file.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/unixccompiler.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/util.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/version.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_distutils/versionpredicate.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_entry_points.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_imp.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_importlib.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_itertools.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_path.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_reqs.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_adapters.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_collections.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_compat.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_functools.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_itertools.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_meta.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_metadata/_text.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/_adapters.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/_common.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/_compat.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/_itertools.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/_legacy.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/abc.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/readers.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/importlib_resources/simple.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/jaraco/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/jaraco/context.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/jaraco/functools.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/jaraco/text/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/more_itertools/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/more_itertools/more.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/more_itertools/recipes.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/ordered_set.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/__about__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/_manylinux.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/_musllinux.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/_structures.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/markers.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/requirements.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/specifiers.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/tags.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/packaging/version.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/actions.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/common.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/core.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/diagram/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/helpers.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/results.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/testing.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/unicode.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/pyparsing/util.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/tomli/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/tomli/_parser.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/tomli/_re.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/tomli/_types.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/typing_extensions.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/_vendor/zipp.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/archive_util.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/build_meta.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/cli-32.exe create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/cli-64.exe create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/cli-arm64.exe create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/cli.exe create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/alias.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/bdist_egg.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/bdist_rpm.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/build.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/build_clib.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/build_ext.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/build_py.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/develop.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/dist_info.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/easy_install.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/editable_wheel.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/egg_info.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/install.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/install_egg_info.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/install_lib.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/install_scripts.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/launcher manifest.xml create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/py36compat.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/register.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/rotate.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/saveopts.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/sdist.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/setopt.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/test.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/upload.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/command/upload_docs.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/_apply_pyprojecttoml.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/error_reporting.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/extra_validations.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/fastjsonschema_exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/fastjsonschema_validations.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/_validate_pyproject/formats.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/expand.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/pyprojecttoml.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/config/setupcfg.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/dep_util.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/depends.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/discovery.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/dist.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/errors.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/extension.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/extern/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/glob.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/gui-32.exe create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/gui-64.exe create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/gui-arm64.exe create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/gui.exe create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/installer.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/launch.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/logging.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/monkey.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/msvc.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/namespaces.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/package_index.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/py34compat.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/sandbox.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/script (dev).tmpl create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/script.tmpl create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/unicode_utils.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/version.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/wheel.py create mode 100644 tapdown/lib/python3.11/site-packages/setuptools/windows_support.py create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket-1.1.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket-1.1.0.dist-info/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket-1.1.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket-1.1.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket-1.1.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket-1.1.0.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket/aiows.py create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket/asgi.py create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket/errors.py create mode 100644 tapdown/lib/python3.11/site-packages/simple_websocket/ws.py create mode 100644 tapdown/lib/python3.11/site-packages/six-1.17.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/six-1.17.0.dist-info/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/six-1.17.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/six-1.17.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/six-1.17.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/six-1.17.0.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/six.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/admin.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/asgi.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/async_admin.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/async_aiopika_manager.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/async_client.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/async_manager.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/async_namespace.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/async_pubsub_manager.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/async_redis_manager.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/async_server.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/async_simple_client.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/base_client.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/base_manager.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/base_namespace.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/base_server.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/client.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/kafka_manager.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/kombu_manager.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/manager.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/middleware.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/msgpack_packet.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/namespace.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/packet.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/pubsub_manager.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/redis_manager.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/server.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/simple_client.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/tornado.py create mode 100644 tapdown/lib/python3.11/site-packages/socketio/zmq_manager.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy-2.0.43.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy-2.0.43.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy-2.0.43.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy-2.0.43.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy-2.0.43.dist-info/licenses/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy-2.0.43.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/connectors/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/connectors/aioodbc.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/connectors/asyncio.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/connectors/pyodbc.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/__init__.py create mode 100755 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/collections.cpython-311-x86_64-linux-gnu.so create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/collections.pyx create mode 100755 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/immutabledict.cpython-311-x86_64-linux-gnu.so create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/immutabledict.pxd create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/immutabledict.pyx create mode 100755 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/processors.cpython-311-x86_64-linux-gnu.so create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/processors.pyx create mode 100755 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/resultproxy.cpython-311-x86_64-linux-gnu.so create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/resultproxy.pyx create mode 100755 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/util.cpython-311-x86_64-linux-gnu.so create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/cyextension/util.pyx create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/_typing.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mssql/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mssql/aioodbc.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mssql/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mssql/information_schema.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mssql/json.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mssql/provision.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mssql/pymssql.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mssql/pyodbc.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/aiomysql.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/asyncmy.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/cymysql.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/dml.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/enumerated.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/expression.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/json.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/mariadb.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/mariadbconnector.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/mysqlconnector.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/mysqldb.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/provision.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/pymysql.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/pyodbc.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/reflection.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/reserved_words.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/mysql/types.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/oracle/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/oracle/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/oracle/cx_oracle.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/oracle/dictionary.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/oracle/oracledb.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/oracle/provision.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/oracle/types.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/oracle/vector.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/_psycopg_common.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/array.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/asyncpg.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/dml.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/ext.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/hstore.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/json.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/named_types.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/operators.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/pg8000.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/pg_catalog.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/provision.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/psycopg.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/psycopg2.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/psycopg2cffi.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/ranges.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/postgresql/types.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/sqlite/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/sqlite/aiosqlite.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/sqlite/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/sqlite/dml.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/sqlite/json.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/sqlite/provision.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/sqlite/pysqlcipher.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/sqlite/pysqlite.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/dialects/type_migration_guidelines.txt create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/_py_processors.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/_py_row.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/_py_util.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/characteristics.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/create.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/cursor.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/default.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/events.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/interfaces.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/mock.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/processors.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/reflection.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/result.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/row.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/strategies.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/url.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/engine/util.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/event/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/event/api.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/event/attr.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/event/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/event/legacy.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/event/registry.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/events.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/exc.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/associationproxy.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/engine.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/exc.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/result.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/scoping.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/asyncio/session.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/automap.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/baked.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/compiler.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/declarative/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/declarative/extensions.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/horizontal_shard.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/hybrid.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/indexable.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/instrumentation.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/mutable.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/mypy/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/mypy/apply.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/mypy/decl_class.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/mypy/infer.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/mypy/names.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/mypy/plugin.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/mypy/util.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/orderinglist.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/ext/serializer.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/future/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/future/engine.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/inspection.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/log.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/_orm_constructors.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/_typing.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/attributes.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/bulk_persistence.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/clsregistry.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/collections.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/context.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/decl_api.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/decl_base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/dependency.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/descriptor_props.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/dynamic.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/evaluator.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/events.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/exc.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/identity.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/instrumentation.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/interfaces.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/loading.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/mapped_collection.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/mapper.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/path_registry.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/persistence.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/properties.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/query.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/relationships.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/scoping.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/session.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/state.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/state_changes.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/strategies.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/strategy_options.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/sync.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/unitofwork.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/util.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/orm/writeonly.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/pool/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/pool/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/pool/events.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/pool/impl.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/schema.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/_dml_constructors.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/_elements_constructors.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/_orm_types.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/_py_util.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/_selectable_constructors.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/_typing.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/annotation.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/cache_key.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/coercions.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/compiler.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/crud.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/ddl.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/default_comparator.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/dml.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/elements.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/events.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/expression.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/functions.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/lambdas.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/naming.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/operators.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/roles.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/schema.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/selectable.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/sqltypes.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/traversals.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/type_api.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/util.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/sql/visitors.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/assertions.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/assertsql.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/asyncio.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/config.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/engines.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/entities.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/exclusions.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/fixtures/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/fixtures/base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/fixtures/mypy.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/fixtures/orm.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/fixtures/sql.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/pickleable.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/plugin/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/plugin/bootstrap.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/plugin/plugin_base.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/plugin/pytestplugin.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/profiling.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/provision.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/requirements.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/schema.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_cte.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_ddl.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_deprecations.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_dialect.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_insert.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_reflection.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_results.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_rowcount.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_select.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_sequence.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_types.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_unicode_ddl.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/suite/test_update_delete.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/util.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/testing/warnings.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/types.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/_collections.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/_concurrency_py3k.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/_has_cy.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/_py_collections.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/compat.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/concurrency.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/deprecations.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/langhelpers.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/preloaded.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/queue.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/tool_support.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/topological.py create mode 100644 tapdown/lib/python3.11/site-packages/sqlalchemy/util/typing.py create mode 100644 tapdown/lib/python3.11/site-packages/typing_extensions-4.15.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/typing_extensions-4.15.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/typing_extensions-4.15.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/typing_extensions-4.15.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/typing_extensions-4.15.0.dist-info/licenses/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/typing_extensions.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug-3.1.3.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug-3.1.3.dist-info/LICENSE.txt create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug-3.1.3.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug-3.1.3.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug-3.1.3.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/_internal.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/_reloader.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/accept.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/auth.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/cache_control.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/csp.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/etag.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/file_storage.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/headers.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/mixins.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/range.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/datastructures/structures.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/debug/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/debug/console.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/debug/repr.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/debug/shared/ICON_LICENSE.md create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/debug/shared/console.png create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/debug/shared/debugger.js create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/debug/shared/less.png create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/debug/shared/more.png create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/debug/shared/style.css create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/debug/tbtools.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/formparser.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/http.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/local.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/middleware/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/middleware/dispatcher.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/middleware/http_proxy.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/middleware/lint.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/middleware/profiler.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/middleware/proxy_fix.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/middleware/shared_data.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/routing/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/routing/converters.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/routing/exceptions.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/routing/map.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/routing/matcher.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/routing/rules.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/sansio/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/sansio/http.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/sansio/multipart.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/sansio/request.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/sansio/response.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/sansio/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/security.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/serving.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/test.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/testapp.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/urls.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/user_agent.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/utils.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/wrappers/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/wrappers/request.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/wrappers/response.py create mode 100644 tapdown/lib/python3.11/site-packages/werkzeug/wsgi.py create mode 100644 tapdown/lib/python3.11/site-packages/wsproto-1.2.0.dist-info/INSTALLER create mode 100644 tapdown/lib/python3.11/site-packages/wsproto-1.2.0.dist-info/LICENSE create mode 100644 tapdown/lib/python3.11/site-packages/wsproto-1.2.0.dist-info/METADATA create mode 100644 tapdown/lib/python3.11/site-packages/wsproto-1.2.0.dist-info/RECORD create mode 100644 tapdown/lib/python3.11/site-packages/wsproto-1.2.0.dist-info/WHEEL create mode 100644 tapdown/lib/python3.11/site-packages/wsproto-1.2.0.dist-info/top_level.txt create mode 100644 tapdown/lib/python3.11/site-packages/wsproto/__init__.py create mode 100644 tapdown/lib/python3.11/site-packages/wsproto/connection.py create mode 100644 tapdown/lib/python3.11/site-packages/wsproto/events.py create mode 100644 tapdown/lib/python3.11/site-packages/wsproto/extensions.py create mode 100644 tapdown/lib/python3.11/site-packages/wsproto/frame_protocol.py create mode 100644 tapdown/lib/python3.11/site-packages/wsproto/handshake.py create mode 100644 tapdown/lib/python3.11/site-packages/wsproto/py.typed create mode 100644 tapdown/lib/python3.11/site-packages/wsproto/typing.py create mode 100644 tapdown/lib/python3.11/site-packages/wsproto/utilities.py create mode 120000 tapdown/lib64 create mode 100644 tapdown/pyvenv.cfg create mode 100644 templates/about.html create mode 100644 templates/admin_inquiries.html create mode 100644 templates/base.html create mode 100644 templates/contact.html create mode 100644 templates/form.html create mode 100644 templates/host.html create mode 100644 templates/host_lobby.html create mode 100644 templates/host_responses.html create mode 100644 templates/host_room.html create mode 100644 templates/index.html create mode 100644 templates/play.html create mode 100644 templates/play_ai.html create mode 100644 templates/queue.html create mode 100644 templates/services.html create mode 100644 templates/survey.html create mode 100644 templates/thanks.html create mode 100644 templates/work.html create mode 100644 wsgi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..333a70b --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +.env +*.pyc +__pycache__/ +venv/ +*.log +cache/ +instance/ diff --git a/app.py b/app.py new file mode 100644 index 0000000..793dd7c --- /dev/null +++ b/app.py @@ -0,0 +1,336 @@ +# app.py +from __future__ import annotations +import os, json +from datetime import datetime +from typing import Dict, Any +from urllib.parse import urlencode, urlsplit, urlunsplit, parse_qsl +from flask import Flask, render_template, request, redirect, url_for, jsonify, flash, Response +from sqlalchemy import create_engine, text, Column, Integer, String, DateTime, JSON +from sqlalchemy.orm import declarative_base, sessionmaker, scoped_session +from sqlalchemy.exc import SQLAlchemyError +from functools import wraps +import hmac +from werkzeug.security import check_password_hash + +# ----------------------------------------------------------------------------- +# App +# ----------------------------------------------------------------------------- +app = Flask(__name__, static_folder="static", static_url_path="/static") + +BRAND = "BrookHaven Technologies" +TAGLINE = "Fast to prototype. Safe to scale." + + + +# --- Personal contact (override via env) --- +CONTACT = { + "name": os.environ.get("BH_CONTACT_NAME", "Benjamin Mosley"), + "title": os.environ.get("BH_CONTACT_TITLE", "Founder, BrookHaven Technologies"), + "email": os.environ.get("BH_CONTACT_EMAIL", "ben@bennyshouse.net"), + "phone": os.environ.get("BH_CONTACT_PHONE", "(806) 655 2300)"), + "city": os.environ.get("BH_CONTACT_CITY", "Canyon / Amarillo / Borger / Remote"), + "cal": os.environ.get("BH_CONTACT_CAL", "https://calendly.com/bennyshouse24/30min"), + "link": os.environ.get("BH_CONTACT_LINK", "https://www.linkedin.com/in/benjamin-mosley-849643329/"), + "site": os.environ.get("BH_CONTACT_SITE", "https://bennyshouse.net"), + "hours": os.environ.get("BH_CONTACT_HOURS", "Mon–Fri, 9a–5p CT"), +} + + + +app.config.update( + SECRET_KEY=os.environ.get("APP_SECRET_KEY", "dev"), + SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE="Lax", + SESSION_COOKIE_SECURE=bool(int(os.environ.get("COOKIE_SECURE", "1"))), # set 1 in prod with HTTPS +) + +# Admin credentials (env-driven) +ADMIN_USER = os.environ.get("BH_ADMIN_USER", "admin") +ADMIN_PW_HASH = os.environ.get("BH_ADMIN_PASSWORD_HASH", "32768:8:1$pgll8a2zdtxky50G$8ef13bb775569f480da14618433b7b80a93f5cb3ef99b67878ddfb058d39e858f05d81b25c88365737d81400ee287a156c76de7b51aed33ea667030f7a83e10d") # pbkdf2 hash +ADMIN_BEARER = os.environ.get("BH_ADMIN_BEARER", "") # optional static token + + +# ----------------------------------------------------------------------------- +# DB (MariaDB) +# ----------------------------------------------------------------------------- +DB_URL = os.environ.get("DB_URL", "mysql+pymysql://tapdown:Swaows.1234@127.0.0.1/tapdown") + +engine = create_engine( + DB_URL, + pool_size=10, + max_overflow=20, + pool_recycle=1800, + pool_pre_ping=True, + isolation_level="READ COMMITTED", + future=True, +) +SessionLocal = scoped_session(sessionmaker(bind=engine, expire_on_commit=False, future=True)) +Base = declarative_base() + +class Inquiry(Base): + __tablename__ = "bh_inquiries" + id = Column(Integer, primary_key=True, autoincrement=True) + name = Column(String(160), nullable=False) + email = Column(String(200), nullable=False) + message = Column(String(4000), nullable=False) + nda = Column(String(8), nullable=False, default="no") # yes|no + meta = Column(JSON, nullable=False, default={}) # e.g. user agent + created_at = Column(DateTime, nullable=False, default=datetime.utcnow) + +_tables_ready = False +@app.before_request +def ensure_tables(): + global _tables_ready + if _tables_ready: + return + try: + with engine.begin() as conn: + Base.metadata.create_all(conn) + _tables_ready = True + except SQLAlchemyError: + app.logger.exception("DB init failed; continuing without DB") + +@app.teardown_appcontext +def remove_session(_=None): + SessionLocal.remove() + +# ----------------------------------------------------------------------------- +# Helpers +# ----------------------------------------------------------------------------- +def with_utm(url: str, extra: Dict[str, str] | None = None) -> str: + scheme, netloc, path, query, frag = urlsplit(url) + q = dict(parse_qsl(query)) + q.update(extra or {}) + return urlunsplit((scheme, netloc, path, urlencode(q), frag)) + + + +app.config.update( + SECRET_KEY=os.environ.get("APP_SECRET_KEY", "dev"), + SESSION_COOKIE_HTTPONLY=True, + SESSION_COOKIE_SAMESITE="Lax", + SESSION_COOKIE_SECURE=bool(int(os.environ.get("COOKIE_SECURE", "0"))), # set 1 in prod with HTTPS +) + +def _is_admin_request(): + # 1) Bearer token (e.g., for automation or CSV curl) + authz = request.headers.get("Authorization", "") + if ADMIN_BEARER and authz.startswith("Bearer "): + token = authz[7:].strip() + if hmac.compare_digest(token, ADMIN_BEARER): + return True + + # 2) HTTP Basic for humans + auth = request.authorization + if auth and ADMIN_PW_HASH and auth.username == ADMIN_USER: + try: + if check_password_hash(ADMIN_PW_HASH, auth.password): + return True + except Exception: + pass + return False + +# ----------------------------------------------------------------------------- +# Routes (pages) +# ----------------------------------------------------------------------------- +@app.get("/") +def home(): + return render_template("index.html", brand=BRAND, tagline=TAGLINE) + + +def require_admin(view): + @wraps(view) + def _wrapped(*args, **kwargs): + if _is_admin_request(): + return view(*args, **kwargs) + return Response( + "Authentication required", + 401, + {"WWW-Authenticate": 'Basic realm="BrookHaven Admin"'}, + ) + return _wrapped + +@app.get("/about") +def about(): + return render_template("about.html", brand=BRAND) + +@app.get("/services") +def services(): + return render_template("services.html", brand=BRAND) + +@app.get("/work") +def work(): + # Example case studies (could be a JSON file later) + cases = [ + { + "title": "Tapdown Showdown — Cyber Sale Activation", + "desc": "Survey-powered mini-game; branded UI, MariaDB analytics, CPU mode for flaky networks.", + "bullets": ["90%+ survey completion", "UTM funnels to promo pages", "Works offline/kiosk"], + "image": "/static/brookhaven-case.jpg", + }, + { + "title": "Kiosk Checkout Prototype", + "desc": "Self-serve event checkout with QR receipts and local-first sync.", + "bullets": ["Local cache", "Queue-busting UX", "Auto export"], + "image": "/static/brookhaven-kiosk.jpg", + }, + ] + return render_template("work.html", brand=BRAND, cases=cases) + +@app.context_processor +def inject_contact(): + return {"CONTACT": CONTACT} + + +@app.get("/contact.vcf") +def contact_vcf(): + # Generate a simple vCard 3.0 + n = CONTACT["name"] + parts = n.split(" ", 1) + last = parts[-1] if len(parts) > 1 else parts[0] + first = parts[0] + phone = CONTACT["phone"].replace(" ", "") + email = CONTACT["email"] + org = BRAND + title = CONTACT["title"] + url = CONTACT["site"] + city = CONTACT["city"] + + vcard = f"""BEGIN:VCARD +VERSION:3.0 +N:{last};{first};;; +FN:{n} +ORG:{org} +TITLE:{title} +TEL;TYPE=CELL,VOICE:{phone} +EMAIL;TYPE=INTERNET:{email} +URL:{url} +ADR;TYPE=WORK:;;{city};;;; +END:VCARD +""" + return (vcard, 200, { + "Content-Type": "text/vcard; charset=utf-8", + "Content-Disposition": 'attachment; filename="brookhaven-contact.vcf"', + }) + + +@app.get("/contact") +def contact(): + return render_template("contact.html", brand=BRAND) + +@app.post("/contact") +def contact_post(): + name = (request.form.get("name") or "").strip() + email = (request.form.get("email") or "").strip() + message = (request.form.get("message") or "").strip() + nda = "yes" if request.form.get("nda") in ("on", "yes", "true") else "no" + + if not name or not email or not message: + flash("Please fill name, email, and a short description.", "error") + return redirect(url_for("contact")) + + # Persist (best-effort) + meta = {"ua": request.headers.get("User-Agent", ""), "ip": request.remote_addr} + db = SessionLocal() + try: + db.add(Inquiry(name=name, email=email, message=message, nda=nda, meta=meta)) + db.commit() + except SQLAlchemyError: + db.rollback() + app.logger.exception("Failed to write inquiry") + # keep going anyway to the thank-you page + + return redirect(url_for("thanks")) + +@app.get("/thanks") +def thanks(): + return render_template("thanks.html", brand=BRAND) + +# ----------------------------------------------------------------------------- +# Admin (read-only) +# ----------------------------------------------------------------------------- +@app.get("/admin/inquiries") +@require_admin +def admin_inquiries(): + page = max(1, int(request.args.get("page", 1))) + per_page = 25 + offset = (page - 1) * per_page + db = SessionLocal() + try: + rows = db.execute( + text("""SELECT id,name,email,message,nda,meta,created_at + FROM bh_inquiries ORDER BY created_at DESC + LIMIT :limit OFFSET :offset"""), + {"limit": per_page, "offset": offset} + ).mappings().all() + total = db.execute(text("SELECT COUNT(*) FROM bh_inquiries")).scalar_one() + except Exception: + app.logger.exception("Query failed") + rows, total = [], 0 + finally: + db.close() + + # Convert meta JSON (string from PyMySQL) to dict + def parse_json(val): + if isinstance(val, dict): return val + if isinstance(val, str) and val: + try: return json.loads(val) + except Exception: return {} + return {} + processed = [] + for r in rows: + processed.append({ + "id": r["id"], "name": r["name"], "email": r["email"], + "message": r["message"], "nda": r["nda"], + "meta": parse_json(r["meta"]), + "created_at": r["created_at"], + }) + pages = (total // per_page) + (1 if total % per_page else 0) + return render_template("admin_inquiries.html", + rows=processed, page=page, pages=pages, total=total, brand=BRAND) + +@app.get("/admin/inquiries.csv") +@require_admin +def admin_inquiries_csv(): + db = SessionLocal() + try: + rows = db.execute( + text("""SELECT id,name,email,message,nda,meta,created_at + FROM bh_inquiries ORDER BY created_at DESC""") + ).mappings().all() + finally: + db.close() + + import csv, io + buf = io.StringIO() + w = csv.writer(buf) + w.writerow(["id","name","email","nda","message","ua","ip","created_at"]) + for r in rows: + meta = r["meta"] + if isinstance(meta, str): + try: + meta = json.loads(meta) + except Exception: + meta = {} + w.writerow([ + r["id"], r["name"], r["email"], r["nda"], + (r["message"] or "").replace("\n"," ").strip(), + (meta or {}).get("ua",""), (meta or {}).get("ip",""), + r["created_at"], + ]) + out = buf.getvalue() + return out, 200, { + "Content-Type": "text/csv; charset=utf-8", + "Content-Disposition": "attachment; filename=brookhaven_inquiries.csv" + } + +# ----------------------------------------------------------------------------- +# Utilities +# ----------------------------------------------------------------------------- +@app.get("/healthz") +def healthz(): + return jsonify({"ok": True, "brand": BRAND}), 200 + +if __name__ == "__main__": + app.run(host="0.0.0.0", port=5050, debug=False) + diff --git a/history/main.html b/history/main.html new file mode 100644 index 0000000..a28712b --- /dev/null +++ b/history/main.html @@ -0,0 +1,253 @@ + + + + + + Indie Grid — Chunky Win95 / Aero + + + + + + + + + + +
+
+ +
+ Indie Grid +
+
+ + + +
+
+
+ + +
+ +
+ +
+ +
+ +
+
+
+ Gaming +
+
+ + + +
+
+ +
+ + + chess +
+ +
+

A chunky Aero/95 take on the Indie card.

+
+ Chunky card image +
+

Big borders, beveled edges, and frosted panels bring that throwback desktop feel.

+ READ MORE » +
+
+ + +
+
+ Code +
+ + + +
+
+
+ + + resource +
+
+

Tower defense… but Flexbox.

+
+ Flexbox +
+

Train display order, growth, and basis—rendered like a 1995 dialog box.

+ START WAVE 1 » +
+
+
+ + + +
+ + + +
+ + + + + + + + diff --git a/static/United_Flyer.jpg b/static/United_Flyer.jpg new file mode 100644 index 0000000000000000000000000000000000000000..c9510d92932ab9b76070b06605473f34253c6d63 GIT binary patch literal 494402 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 z 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 literal 0 HcmV?d00001 diff --git a/tapdown/bin/Activate.ps1 b/tapdown/bin/Activate.ps1 new file mode 100644 index 0000000..b49d77b --- /dev/null +++ b/tapdown/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/tapdown/bin/activate b/tapdown/bin/activate new file mode 100644 index 0000000..7ba1f68 --- /dev/null +++ b/tapdown/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/benny/tapdown +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='(tapdown) '"${PS1:-}" + export PS1 + VIRTUAL_ENV_PROMPT='(tapdown) ' + 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/tapdown/bin/activate.csh b/tapdown/bin/activate.csh new file mode 100644 index 0000000..3a6736c --- /dev/null +++ b/tapdown/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/benny/tapdown + +set _OLD_VIRTUAL_PATH="$PATH" +setenv PATH "$VIRTUAL_ENV/"bin":$PATH" + + +set _OLD_VIRTUAL_PROMPT="$prompt" + +if (! "$?VIRTUAL_ENV_DISABLE_PROMPT") then + set prompt = '(tapdown) '"$prompt" + setenv VIRTUAL_ENV_PROMPT '(tapdown) ' +endif + +alias pydoc python -m pydoc + +rehash diff --git a/tapdown/bin/activate.fish b/tapdown/bin/activate.fish new file mode 100644 index 0000000..449dc70 --- /dev/null +++ b/tapdown/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/benny/tapdown + +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) '(tapdown) ' (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 '(tapdown) ' +end diff --git a/tapdown/bin/flask b/tapdown/bin/flask new file mode 100755 index 0000000..9ac8c17 --- /dev/null +++ b/tapdown/bin/flask @@ -0,0 +1,8 @@ +#!/var/www/benny/tapdown/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/tapdown/bin/gunicorn b/tapdown/bin/gunicorn new file mode 100755 index 0000000..2be37b3 --- /dev/null +++ b/tapdown/bin/gunicorn @@ -0,0 +1,8 @@ +#!/var/www/benny/tapdown/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/tapdown/bin/pip b/tapdown/bin/pip new file mode 100755 index 0000000..7e21037 --- /dev/null +++ b/tapdown/bin/pip @@ -0,0 +1,8 @@ +#!/var/www/benny/tapdown/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/tapdown/bin/pip3 b/tapdown/bin/pip3 new file mode 100755 index 0000000..7e21037 --- /dev/null +++ b/tapdown/bin/pip3 @@ -0,0 +1,8 @@ +#!/var/www/benny/tapdown/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/tapdown/bin/pip3.11 b/tapdown/bin/pip3.11 new file mode 100755 index 0000000..7e21037 --- /dev/null +++ b/tapdown/bin/pip3.11 @@ -0,0 +1,8 @@ +#!/var/www/benny/tapdown/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/tapdown/bin/python b/tapdown/bin/python new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/tapdown/bin/python @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/tapdown/bin/python3 b/tapdown/bin/python3 new file mode 120000 index 0000000..ae65fda --- /dev/null +++ b/tapdown/bin/python3 @@ -0,0 +1 @@ +/usr/bin/python3 \ No newline at end of file diff --git a/tapdown/bin/python3.11 b/tapdown/bin/python3.11 new file mode 120000 index 0000000..b8a0adb --- /dev/null +++ b/tapdown/bin/python3.11 @@ -0,0 +1 @@ +python3 \ No newline at end of file diff --git a/tapdown/include/site/python3.11/greenlet/greenlet.h b/tapdown/include/site/python3.11/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/LICENSE b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/LICENSE new file mode 100644 index 0000000..f5c10ab --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/LICENSE @@ -0,0 +1,20 @@ +The MIT License (MIT) + +Copyright (c) 2014 Miguel Grinberg + +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/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/METADATA new file mode 100644 index 0000000..da2431e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/METADATA @@ -0,0 +1,76 @@ +Metadata-Version: 2.1 +Name: Flask-SocketIO +Version: 5.5.1 +Summary: Socket.IO integration for Flask applications +Author-email: Miguel Grinberg +Project-URL: Homepage, https://github.com/miguelgrinberg/flask-socketio +Project-URL: Bug Tracker, https://github.com/miguelgrinberg/flask-socketio/issues +Classifier: Environment :: Web Environment +Classifier: Intended Audience :: Developers +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Classifier: Operating System :: OS Independent +Requires-Python: >=3.6 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: Flask>=0.9 +Requires-Dist: python-socketio>=5.12.0 +Provides-Extra: docs +Requires-Dist: sphinx; extra == "docs" + +Flask-SocketIO +============== + +[![Build status](https://github.com/miguelgrinberg/flask-socketio/workflows/build/badge.svg)](https://github.com/miguelgrinberg/Flask-SocketIO/actions) [![codecov](https://codecov.io/gh/miguelgrinberg/flask-socketio/branch/main/graph/badge.svg)](https://codecov.io/gh/miguelgrinberg/flask-socketio) + +Socket.IO integration for Flask applications. + +Sponsors +-------- + +The following organizations are funding this project: + +![Socket.IO](https://images.opencollective.com/socketio/050e5eb/logo/64.png)
[Socket.IO](https://socket.io) | [Add your company here!](https://github.com/sponsors/miguelgrinberg)| +-|- + +Many individual sponsors also support this project through small ongoing contributions. Why not [join them](https://github.com/sponsors/miguelgrinberg)? + +Installation +------------ + +You can install this package as usual with pip: + + pip install flask-socketio + +Example +------- + +```py +from flask import Flask, render_template +from flask_socketio import SocketIO, emit + +app = Flask(__name__) +app.config['SECRET_KEY'] = 'secret!' +socketio = SocketIO(app) + +@app.route('/') +def index(): + return render_template('index.html') + +@socketio.event +def my_event(message): + emit('my response', {'data': 'got it!'}) + +if __name__ == '__main__': + socketio.run(app) +``` + +Resources +--------- + +- [Tutorial](http://blog.miguelgrinberg.com/post/easy-websockets-with-flask-and-gevent) +- [Documentation](http://flask-socketio.readthedocs.io/en/latest/) +- [PyPI](https://pypi.python.org/pypi/Flask-SocketIO) +- [Change Log](https://github.com/miguelgrinberg/Flask-SocketIO/blob/main/CHANGES.md) +- Questions? See the [questions](https://stackoverflow.com/questions/tagged/flask-socketio) others have asked on Stack Overflow, or [ask](https://stackoverflow.com/questions/ask?tags=python+flask-socketio+python-socketio) your own question. + diff --git a/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/RECORD new file mode 100644 index 0000000..ae659f9 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/RECORD @@ -0,0 +1,13 @@ +Flask_SocketIO-5.5.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +Flask_SocketIO-5.5.1.dist-info/LICENSE,sha256=aNCWbkgKjS_T1cJtACyZbvCM36KxWnfQ0LWTuavuYKQ,1082 +Flask_SocketIO-5.5.1.dist-info/METADATA,sha256=7YA8ZKizrtJiaCqqdDiTU6t1xWWdTmNw3CqBxSMcW3k,2635 +Flask_SocketIO-5.5.1.dist-info/RECORD,, +Flask_SocketIO-5.5.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +Flask_SocketIO-5.5.1.dist-info/WHEEL,sha256=A3WOREP4zgxI0fKrHUG8DC8013e3dK3n7a6HDbcEIwE,91 +Flask_SocketIO-5.5.1.dist-info/top_level.txt,sha256=C1ugzQBJ3HHUJsWGzyt70XRVOX-y4CUAR8MWKjwJOQ8,15 +flask_socketio/__init__.py,sha256=5hN0LE0hfGMUDcX4FheZrtXERJ1IBEPagv0pgeqdtlU,54904 +flask_socketio/__pycache__/__init__.cpython-311.pyc,, +flask_socketio/__pycache__/namespace.cpython-311.pyc,, +flask_socketio/__pycache__/test_client.cpython-311.pyc,, +flask_socketio/namespace.py,sha256=UkVryJvFYgnCMKWSF35GVfGdyh2cXRDyRbfmEPPchVA,2329 +flask_socketio/test_client.py,sha256=rClk02TSRqgidH8IyeohspKVKdpRx7gcZBjg1YUtZpA,11026 diff --git a/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/REQUESTED b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/WHEEL new file mode 100644 index 0000000..1e3dcb1 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (75.7.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/top_level.txt b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/top_level.txt new file mode 100755 index 0000000..ba82ec3 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/Flask_SocketIO-5.5.1.dist-info/top_level.txt @@ -0,0 +1 @@ +flask_socketio diff --git a/tapdown/lib/python3.11/site-packages/_distutils_hack/__init__.py b/tapdown/lib/python3.11/site-packages/_distutils_hack/__init__.py new file mode 100644 index 0000000..f987a53 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/_distutils_hack/override.py b/tapdown/lib/python3.11/site-packages/_distutils_hack/override.py new file mode 100644 index 0000000..2cc433a --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/_distutils_hack/override.py @@ -0,0 +1 @@ +__import__('_distutils_hack').do_override() diff --git a/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/LICENSE b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/LICENSE new file mode 100644 index 0000000..d1cc6f8 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/LICENSE @@ -0,0 +1,376 @@ +Mozilla Public License Version 2.0 +================================== + +Copyright 2009-2024 Joshua Bronson. All rights reserved. + + +1. Definitions +-------------- + +1.1. "Contributor" + means each individual or legal entity that creates, contributes to + the creation of, or owns Covered Software. + +1.2. "Contributor Version" + means the combination of the Contributions of others (if any) used + by a Contributor and that particular Contributor's Contribution. + +1.3. "Contribution" + means Covered Software of a particular Contributor. + +1.4. "Covered Software" + means Source Code Form to which the initial Contributor has attached + the notice in Exhibit A, the Executable Form of such Source Code + Form, and Modifications of such Source Code Form, in each case + including portions thereof. + +1.5. "Incompatible With Secondary Licenses" + means + + (a) that the initial Contributor has attached the notice described + in Exhibit B to the Covered Software; or + + (b) that the Covered Software was made available under the terms of + version 1.1 or earlier of the License, but not also under the + terms of a Secondary License. + +1.6. "Executable Form" + means any form of the work other than Source Code Form. + +1.7. "Larger Work" + means a work that combines Covered Software with other material, in + a separate file or files, that is not Covered Software. + +1.8. "License" + means this document. + +1.9. "Licensable" + means having the right to grant, to the maximum extent possible, + whether at the time of the initial grant or subsequently, any and + all of the rights conveyed by this License. + +1.10. "Modifications" + means any of the following: + + (a) any file in Source Code Form that results from an addition to, + deletion from, or modification of the contents of Covered + Software; or + + (b) any new file in Source Code Form that contains any Covered + Software. + +1.11. "Patent Claims" of a Contributor + means any patent claim(s), including without limitation, method, + process, and apparatus claims, in any patent Licensable by such + Contributor that would be infringed, but for the grant of the + License, by the making, using, selling, offering for sale, having + made, import, or transfer of either its Contributions or its + Contributor Version. + +1.12. "Secondary License" + means either the GNU General Public License, Version 2.0, the GNU + Lesser General Public License, Version 2.1, the GNU Affero General + Public License, Version 3.0, or any later versions of those + licenses. + +1.13. "Source Code Form" + means the form of the work preferred for making modifications. + +1.14. "You" (or "Your") + means an individual or a legal entity exercising rights under this + License. For legal entities, "You" includes any entity that + controls, is controlled by, or is under common control with You. For + purposes of this definition, "control" means (a) the power, direct + or indirect, to cause the direction or management of such entity, + whether by contract or otherwise, or (b) ownership of more than + fifty percent (50%) of the outstanding shares or beneficial + ownership of such entity. + +2. License Grants and Conditions +-------------------------------- + +2.1. Grants + +Each Contributor hereby grants You a world-wide, royalty-free, +non-exclusive license: + +(a) under intellectual property rights (other than patent or trademark) + Licensable by such Contributor to use, reproduce, make available, + modify, display, perform, distribute, and otherwise exploit its + Contributions, either on an unmodified basis, with Modifications, or + as part of a Larger Work; and + +(b) under Patent Claims of such Contributor to make, use, sell, offer + for sale, have made, import, and otherwise transfer either its + Contributions or its Contributor Version. + +2.2. Effective Date + +The licenses granted in Section 2.1 with respect to any Contribution +become effective for each Contribution on the date the Contributor first +distributes such Contribution. + +2.3. Limitations on Grant Scope + +The licenses granted in this Section 2 are the only rights granted under +this License. No additional rights or licenses will be implied from the +distribution or licensing of Covered Software under this License. +Notwithstanding Section 2.1(b) above, no patent license is granted by a +Contributor: + +(a) for any code that a Contributor has removed from Covered Software; + or + +(b) for infringements caused by: (i) Your and any other third party's + modifications of Covered Software, or (ii) the combination of its + Contributions with other software (except as part of its Contributor + Version); or + +(c) under Patent Claims infringed by Covered Software in the absence of + its Contributions. + +This License does not grant any rights in the trademarks, service marks, +or logos of any Contributor (except as may be necessary to comply with +the notice requirements in Section 3.4). + +2.4. Subsequent Licenses + +No Contributor makes additional grants as a result of Your choice to +distribute the Covered Software under a subsequent version of this +License (see Section 10.2) or under the terms of a Secondary License (if +permitted under the terms of Section 3.3). + +2.5. Representation + +Each Contributor represents that the Contributor believes its +Contributions are its original creation(s) or it has sufficient rights +to grant the rights to its Contributions conveyed by this License. + +2.6. Fair Use + +This License is not intended to limit any rights You have under +applicable copyright doctrines of fair use, fair dealing, or other +equivalents. + +2.7. Conditions + +Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted +in Section 2.1. + +3. Responsibilities +------------------- + +3.1. Distribution of Source Form + +All distribution of Covered Software in Source Code Form, including any +Modifications that You create or to which You contribute, must be under +the terms of this License. You must inform recipients that the Source +Code Form of the Covered Software is governed by the terms of this +License, and how they can obtain a copy of this License. You may not +attempt to alter or restrict the recipients' rights in the Source Code +Form. + +3.2. Distribution of Executable Form + +If You distribute Covered Software in Executable Form then: + +(a) such Covered Software must also be made available in Source Code + Form, as described in Section 3.1, and You must inform recipients of + the Executable Form how they can obtain a copy of such Source Code + Form by reasonable means in a timely manner, at a charge no more + than the cost of distribution to the recipient; and + +(b) You may distribute such Executable Form under the terms of this + License, or sublicense it under different terms, provided that the + license for the Executable Form does not attempt to limit or alter + the recipients' rights in the Source Code Form under this License. + +3.3. Distribution of a Larger Work + +You may create and distribute a Larger Work under terms of Your choice, +provided that You also comply with the requirements of this License for +the Covered Software. If the Larger Work is a combination of Covered +Software with a work governed by one or more Secondary Licenses, and the +Covered Software is not Incompatible With Secondary Licenses, this +License permits You to additionally distribute such Covered Software +under the terms of such Secondary License(s), so that the recipient of +the Larger Work may, at their option, further distribute the Covered +Software under the terms of either this License or such Secondary +License(s). + +3.4. Notices + +You may not remove or alter the substance of any license notices +(including copyright notices, patent notices, disclaimers of warranty, +or limitations of liability) contained within the Source Code Form of +the Covered Software, except that You may alter any license notices to +the extent required to remedy known factual inaccuracies. + +3.5. Application of Additional Terms + +You may choose to offer, and to charge a fee for, warranty, support, +indemnity or liability obligations to one or more recipients of Covered +Software. However, You may do so only on Your own behalf, and not on +behalf of any Contributor. You must make it absolutely clear that any +such warranty, support, indemnity, or liability obligation is offered by +You alone, and You hereby agree to indemnify every Contributor for any +liability incurred by such Contributor as a result of warranty, support, +indemnity or liability terms You offer. You may include additional +disclaimers of warranty and limitations of liability specific to any +jurisdiction. + +4. Inability to Comply Due to Statute or Regulation +--------------------------------------------------- + +If it is impossible for You to comply with any of the terms of this +License with respect to some or all of the Covered Software due to +statute, judicial order, or regulation then You must: (a) comply with +the terms of this License to the maximum extent possible; and (b) +describe the limitations and the code they affect. Such description must +be placed in a text file included with all distributions of the Covered +Software under this License. Except to the extent prohibited by statute +or regulation, such description must be sufficiently detailed for a +recipient of ordinary skill to be able to understand it. + +5. Termination +-------------- + +5.1. The rights granted under this License will terminate automatically +if You fail to comply with any of its terms. However, if You become +compliant, then the rights granted under this License from a particular +Contributor are reinstated (a) provisionally, unless and until such +Contributor explicitly and finally terminates Your grants, and (b) on an +ongoing basis, if such Contributor fails to notify You of the +non-compliance by some reasonable means prior to 60 days after You have +come back into compliance. Moreover, Your grants from a particular +Contributor are reinstated on an ongoing basis if such Contributor +notifies You of the non-compliance by some reasonable means, this is the +first time You have received notice of non-compliance with this License +from such Contributor, and You become compliant prior to 30 days after +Your receipt of the notice. + +5.2. If You initiate litigation against any entity by asserting a patent +infringement claim (excluding declaratory judgment actions, +counter-claims, and cross-claims) alleging that a Contributor Version +directly or indirectly infringes any patent, then the rights granted to +You by any and all Contributors for the Covered Software under Section +2.1 of this License shall terminate. + +5.3. In the event of termination under Sections 5.1 or 5.2 above, all +end user license agreements (excluding distributors and resellers) which +have been validly granted by You or Your distributors under this License +prior to termination shall survive termination. + +************************************************************************ +* * +* 6. Disclaimer of Warranty * +* ------------------------- * +* * +* Covered Software is provided under this License on an "as is" * +* basis, without warranty of any kind, either expressed, implied, or * +* statutory, including, without limitation, warranties that the * +* Covered Software is free of defects, merchantable, fit for a * +* particular purpose or non-infringing. The entire risk as to the * +* quality and performance of the Covered Software is with You. * +* Should any Covered Software prove defective in any respect, You * +* (not any Contributor) assume the cost of any necessary servicing, * +* repair, or correction. This disclaimer of warranty constitutes an * +* essential part of this License. No use of any Covered Software is * +* authorized under this License except under this disclaimer. * +* * +************************************************************************ + +************************************************************************ +* * +* 7. Limitation of Liability * +* -------------------------- * +* * +* Under no circumstances and under no legal theory, whether tort * +* (including negligence), contract, or otherwise, shall any * +* Contributor, or anyone who distributes Covered Software as * +* permitted above, be liable to You for any direct, indirect, * +* special, incidental, or consequential damages of any character * +* including, without limitation, damages for lost profits, loss of * +* goodwill, work stoppage, computer failure or malfunction, or any * +* and all other commercial damages or losses, even if such party * +* shall have been informed of the possibility of such damages. This * +* limitation of liability shall not apply to liability for death or * +* personal injury resulting from such party's negligence to the * +* extent applicable law prohibits such limitation. Some * +* jurisdictions do not allow the exclusion or limitation of * +* incidental or consequential damages, so this exclusion and * +* limitation may not apply to You. * +* * +************************************************************************ + +8. Litigation +------------- + +Any litigation relating to this License may be brought only in the +courts of a jurisdiction where the defendant maintains its principal +place of business and such litigation shall be governed by laws of that +jurisdiction, without reference to its conflict-of-law provisions. +Nothing in this Section shall prevent a party's ability to bring +cross-claims or counter-claims. + +9. Miscellaneous +---------------- + +This License represents the complete agreement concerning the subject +matter hereof. If any provision of this License is held to be +unenforceable, such provision shall be reformed only to the extent +necessary to make it enforceable. Any law or regulation which provides +that the language of a contract shall be construed against the drafter +shall not be used to construe this License against a Contributor. + +10. Versions of the License +--------------------------- + +10.1. New Versions + +Mozilla Foundation is the license steward. Except as provided in Section +10.3, no one other than the license steward has the right to modify or +publish new versions of this License. Each version will be given a +distinguishing version number. + +10.2. Effect of New Versions + +You may distribute the Covered Software under the terms of the version +of the License under which You originally received the Covered Software, +or under the terms of any subsequent version published by the license +steward. + +10.3. Modified Versions + +If you create software not governed by this License, and you want to +create a new license for such software, you may create and use a +modified version of this License if you rename the license and remove +any references to the name of the license steward (except to note that +such modified license differs from this License). + +10.4. Distributing Source Code Form that is Incompatible With Secondary +Licenses + +If You choose to distribute Source Code Form that is Incompatible With +Secondary Licenses under the terms of this version of the License, the +notice described in Exhibit B of this License must be attached. + +Exhibit A - Source Code Form License Notice +------------------------------------------- + + This Source Code Form is subject to the terms of the Mozilla Public + License, v. 2.0. If a copy of the MPL was not distributed with this + file, You can obtain one at http://mozilla.org/MPL/2.0/. + +If it is not possible or desirable to put the notice in a particular +file, then You may include the notice in a location (such as a LICENSE +file in a relevant directory) where a recipient would be likely to look +for such a notice. + +You may add additional accurate notices of copyright ownership. + +Exhibit B - "Incompatible With Secondary Licenses" Notice +--------------------------------------------------------- + + This Source Code Form is "Incompatible With Secondary Licenses", as + defined by the Mozilla Public License, v. 2.0. diff --git a/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/METADATA new file mode 100644 index 0000000..5356d23 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/METADATA @@ -0,0 +1,260 @@ +Metadata-Version: 2.1 +Name: bidict +Version: 0.23.1 +Summary: The bidirectional mapping library for Python. +Author-email: Joshua Bronson +License: MPL 2.0 +Project-URL: Changelog, https://bidict.readthedocs.io/changelog.html +Project-URL: Documentation, https://bidict.readthedocs.io +Project-URL: Funding, https://bidict.readthedocs.io/#sponsoring +Project-URL: Repository, https://github.com/jab/bidict +Keywords: bidict,bimap,bidirectional,dict,dictionary,mapping,collections +Classifier: License :: OSI Approved :: Mozilla Public License 2.0 (MPL 2.0) +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Typing :: Typed +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +License-File: LICENSE + +.. role:: doc +.. (Forward declaration for the "doc" role that Sphinx defines for interop with renderers that + are often used to show this doc and that are unaware of Sphinx (GitHub.com, PyPI.org, etc.). + Use :doc: rather than :ref: here for better interop as well.) + + +bidict +====== + +*The bidirectional mapping library for Python.* + + +Status +------ + +.. image:: https://img.shields.io/pypi/v/bidict.svg + :target: https://pypi.org/project/bidict + :alt: Latest release + +.. image:: https://img.shields.io/readthedocs/bidict/main.svg + :target: https://bidict.readthedocs.io/en/main/ + :alt: Documentation + +.. image:: https://github.com/jab/bidict/actions/workflows/test.yml/badge.svg + :target: https://github.com/jab/bidict/actions/workflows/test.yml?query=branch%3Amain + :alt: GitHub Actions CI status + +.. image:: https://img.shields.io/pypi/l/bidict.svg + :target: https://raw.githubusercontent.com/jab/bidict/main/LICENSE + :alt: License + +.. image:: https://static.pepy.tech/badge/bidict + :target: https://pepy.tech/project/bidict + :alt: PyPI Downloads + +.. image:: https://img.shields.io/badge/GitHub-sponsor-ff69b4 + :target: https://github.com/sponsors/jab + :alt: Sponsor + + +Features +-------- + +- Mature: Depended on by + Google, Venmo, CERN, Baidu, Tencent, + and teams across the world since 2009 + +- Familiar, Pythonic APIs + that are carefully designed for + safety, simplicity, flexibility, and ergonomics + +- Lightweight, with no runtime dependencies + outside Python's standard library + +- Implemented in + concise, well-factored, fully type-hinted Python code + that is optimized for running efficiently + as well as for long-term maintenance and stability + (as well as `joy <#learning-from-bidict>`__) + +- Extensively `documented `__ + +- 100% test coverage + running continuously across all supported Python versions + (including property-based tests and benchmarks) + + +Installation +------------ + +``pip install bidict`` + + +Quick Start +----------- + +.. code:: python + + >>> from bidict import bidict + >>> element_by_symbol = bidict({'H': 'hydrogen'}) + >>> element_by_symbol['H'] + 'hydrogen' + >>> element_by_symbol.inverse['hydrogen'] + 'H' + + +For more usage documentation, +head to the :doc:`intro` [#fn-intro]_ +and proceed from there. + + +Enterprise Support +------------------ + +Enterprise-level support for bidict can be obtained via the +`Tidelift subscription `__ +or by `contacting me directly `__. + +I have a US-based LLC set up for invoicing, +and I have 15+ years of professional experience +delivering software and support to companies successfully. + +You can also sponsor my work through several platforms, including GitHub Sponsors. +See the `Sponsoring <#sponsoring>`__ section below for details, +including rationale and examples of companies +supporting the open source projects they depend on. + + +Voluntary Community Support +--------------------------- + +Please search through already-asked questions and answers +in `GitHub Discussions `__ +and the `issue tracker `__ +in case your question has already been addressed. + +Otherwise, please feel free to +`start a new discussion `__ +or `create a new issue `__ on GitHub +for voluntary community support. + + +Notice of Usage +--------------- + +If you use bidict, +and especially if your usage or your organization is significant in some way, +please let me know in any of the following ways: + +- `star bidict on GitHub `__ +- post in `GitHub Discussions `__ +- `email me `__ + + +Changelog +--------- + +For bidict release notes, see the :doc:`changelog`. [#fn-changelog]_ + + +Release Notifications +--------------------- + +.. duplicated in CHANGELOG.rst: + (Would use `.. include::` but GitHub's renderer doesn't support it.) + +Watch `bidict releases on GitHub `__ +to be notified when new versions of bidict are published. +Click the "Watch" dropdown, choose "Custom", and then choose "Releases". + + +Learning from bidict +-------------------- + +One of the best things about bidict +is that it touches a surprising number of +interesting Python corners, +especially given its small size and scope. + +Check out :doc:`learning-from-bidict` [#fn-learning]_ +if you're interested in learning more. + + +Contributing +------------ + +I have been bidict's sole maintainer +and `active contributor `__ +since I started the project ~15 years ago. + +Your help would be most welcome! +See the :doc:`contributors-guide` [#fn-contributing]_ +for more information. + + +Sponsoring +---------- + +.. duplicated in CONTRIBUTING.rst + (Would use `.. include::` but GitHub's renderer doesn't support it.) + +.. image:: https://img.shields.io/badge/GitHub-sponsor-ff69b4 + :target: https://github.com/sponsors/jab + :alt: Sponsor through GitHub + +Bidict is the product of thousands of hours of my unpaid work +over the 15+ years that I've been the sole maintainer. + +If bidict has helped you or your company accomplish your work, +please sponsor my work through one of the following, +and/or ask your company to do the same: + +- `GitHub `__ +- `PayPal `__ +- `Tidelift `__ +- `thanks.dev `__ +- `Gumroad `__ +- `a support engagement with my LLC <#enterprise-support>`__ + +If you're not sure which to use, GitHub is an easy option, +especially if you already have a GitHub account. +Just choose a monthly or one-time amount, and GitHub handles everything else. +Your bidict sponsorship on GitHub will automatically go +on the same regular bill as any other GitHub charges you pay for. +PayPal is another easy option for one-time contributions. + +See the following for rationale and examples of companies +supporting the open source projects they depend on +in this manner: + +- ``__ +- ``__ +- ``__ + +.. - ``__ +.. - ``__ +.. - ``__ + + +Finding Documentation +--------------------- + +If you're viewing this on ``__, +note that multiple versions of the documentation are available, +and you can choose a different version using the popup menu at the bottom-right. +Please make sure you're viewing the version of the documentation +that corresponds to the version of bidict you'd like to use. + +If you're viewing this on GitHub, PyPI, or some other place +that can't render and link this documentation properly +and are seeing broken links, +try these alternate links instead: + +.. [#fn-intro] ``__ | ``__ + +.. [#fn-changelog] ``__ | ``__ + +.. [#fn-learning] ``__ | ``__ + +.. [#fn-contributing] ``__ | ``__ diff --git a/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/RECORD new file mode 100644 index 0000000..399ed98 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/RECORD @@ -0,0 +1,31 @@ +bidict-0.23.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +bidict-0.23.1.dist-info/LICENSE,sha256=8_U63OyqSNc6ZuI4-lupBstBh2eDtF0ooTRrMULuvZo,16784 +bidict-0.23.1.dist-info/METADATA,sha256=2ovIRm6Df8gdwAMekGqkeBSF5TWj2mv1jpmh4W4ks7o,8704 +bidict-0.23.1.dist-info/RECORD,, +bidict-0.23.1.dist-info/WHEEL,sha256=oiQVh_5PnQM0E3gPdiz09WCNmwiHDMaGer_elqB3coM,92 +bidict-0.23.1.dist-info/top_level.txt,sha256=WuQO02jp0ODioS7sJoaHg3JJ5_3h6Sxo9RITvNGPYmc,7 +bidict/__init__.py,sha256=pL87KsrDpBsl3AG09LQk1t1TSFt0hIJVYa2POMdErN8,4398 +bidict/__pycache__/__init__.cpython-311.pyc,, +bidict/__pycache__/_abc.cpython-311.pyc,, +bidict/__pycache__/_base.cpython-311.pyc,, +bidict/__pycache__/_bidict.cpython-311.pyc,, +bidict/__pycache__/_dup.cpython-311.pyc,, +bidict/__pycache__/_exc.cpython-311.pyc,, +bidict/__pycache__/_frozen.cpython-311.pyc,, +bidict/__pycache__/_iter.cpython-311.pyc,, +bidict/__pycache__/_orderedbase.cpython-311.pyc,, +bidict/__pycache__/_orderedbidict.cpython-311.pyc,, +bidict/__pycache__/_typing.cpython-311.pyc,, +bidict/__pycache__/metadata.cpython-311.pyc,, +bidict/_abc.py,sha256=SMCNdCsmqSWg0OGnMZtnnXY8edjXcyZup5tva4HBm_c,3172 +bidict/_base.py,sha256=YiauA0aj52fNB6cfZ4gBt6OV-CRQoZm7WVhuw1nT-Cg,24439 +bidict/_bidict.py,sha256=Sr-RoEzWOaxpnDRbDJ7ngaGRIsyGnqZgzvR-NyT4jl4,6923 +bidict/_dup.py,sha256=YAn5gWA6lwMBA5A6ebVF19UTZyambGS8WxmbK4TN1Ww,2079 +bidict/_exc.py,sha256=HnD_WgteI5PrXa3zBx9RUiGlgnZTO6CF4nIU9p3-njk,1066 +bidict/_frozen.py,sha256=p4TaRHKeyTs0KmlpwSnZiTlN_CR4J97kAgBpNdZHQMs,1771 +bidict/_iter.py,sha256=zVUx-hJ1M4YuJROoFWRjPKlcaFnyo1AAuRpOaKAFhOQ,1530 +bidict/_orderedbase.py,sha256=M7v5rHa7vrym9Z3DxQBFQDxjnrr39Z8p26V0c1PggoE,8942 +bidict/_orderedbidict.py,sha256=pPnmC19mIISrj8_yjnb-4r_ti1B74tD5eTd08DETNuI,7080 +bidict/_typing.py,sha256=AylMZpBhEFTQegfziPSxfKkKLk7oUsH6o3awDIg2z_k,1289 +bidict/metadata.py,sha256=BMIKu6fBY_OKeV_q48EpumE7MdmFw8rFcdaUz8kcIYk,573 +bidict/py.typed,sha256=RJao5SVFYIp8IfbxhL_SpZkBQYe3XXzPlobSRdh4B_c,16 diff --git a/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/WHEEL new file mode 100644 index 0000000..98c0d20 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: bdist_wheel (0.42.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/top_level.txt b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/top_level.txt new file mode 100644 index 0000000..6ff5b04 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict-0.23.1.dist-info/top_level.txt @@ -0,0 +1 @@ +bidict diff --git a/tapdown/lib/python3.11/site-packages/bidict/__init__.py b/tapdown/lib/python3.11/site-packages/bidict/__init__.py new file mode 100644 index 0000000..07e5ba5 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/__init__.py @@ -0,0 +1,103 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +# ============================================================================ +# * Welcome to the bidict source code * +# ============================================================================ + +# Reading through the code? You'll find a "Code review nav" comment like the one +# below at the top and bottom of the key source files. Follow these cues to take +# a path through the code that's optimized for familiarizing yourself with it. +# +# If you're not reading this on https://github.com/jab/bidict already, go there +# to ensure you have the latest version of the code. While there, you can also +# star the project, watch it for updates, fork the code, and submit an issue or +# pull request with any proposed changes. More information can be found linked +# from README.rst, which is also shown on https://github.com/jab/bidict. + +# * Code review nav * +# ============================================================================ +# Current: __init__.py Next: _abc.py → +# ============================================================================ + + +"""The bidirectional mapping library for Python. + +---- + +bidict by example: + +.. code-block:: python + + >>> from bidict import bidict + >>> element_by_symbol = bidict({'H': 'hydrogen'}) + >>> element_by_symbol['H'] + 'hydrogen' + >>> element_by_symbol.inverse['hydrogen'] + 'H' + + +Please see https://github.com/jab/bidict for the most up-to-date code and +https://bidict.readthedocs.io for the most up-to-date documentation +if you are reading this elsewhere. + +---- + +.. :copyright: (c) 2009-2024 Joshua Bronson. +.. :license: MPLv2. See LICENSE for details. +""" + +# Use private aliases to not re-export these publicly (for Sphinx automodule with imported-members). +from __future__ import annotations as _annotations + +from contextlib import suppress as _suppress + +from ._abc import BidirectionalMapping as BidirectionalMapping +from ._abc import MutableBidirectionalMapping as MutableBidirectionalMapping +from ._base import BidictBase as BidictBase +from ._base import BidictKeysView as BidictKeysView +from ._base import GeneratedBidictInverse as GeneratedBidictInverse +from ._bidict import MutableBidict as MutableBidict +from ._bidict import bidict as bidict +from ._dup import DROP_NEW as DROP_NEW +from ._dup import DROP_OLD as DROP_OLD +from ._dup import ON_DUP_DEFAULT as ON_DUP_DEFAULT +from ._dup import ON_DUP_DROP_OLD as ON_DUP_DROP_OLD +from ._dup import ON_DUP_RAISE as ON_DUP_RAISE +from ._dup import RAISE as RAISE +from ._dup import OnDup as OnDup +from ._dup import OnDupAction as OnDupAction +from ._exc import BidictException as BidictException +from ._exc import DuplicationError as DuplicationError +from ._exc import KeyAndValueDuplicationError as KeyAndValueDuplicationError +from ._exc import KeyDuplicationError as KeyDuplicationError +from ._exc import ValueDuplicationError as ValueDuplicationError +from ._frozen import frozenbidict as frozenbidict +from ._iter import inverted as inverted +from ._orderedbase import OrderedBidictBase as OrderedBidictBase +from ._orderedbidict import OrderedBidict as OrderedBidict +from .metadata import __author__ as __author__ +from .metadata import __copyright__ as __copyright__ +from .metadata import __description__ as __description__ +from .metadata import __license__ as __license__ +from .metadata import __url__ as __url__ +from .metadata import __version__ as __version__ + + +# Set __module__ of re-exported classes to the 'bidict' top-level module, so that e.g. +# 'bidict.bidict' shows up as 'bidict.bidict` rather than 'bidict._bidict.bidict'. +for _obj in tuple(locals().values()): # pragma: no cover + if not getattr(_obj, '__module__', '').startswith('bidict.'): + continue + with _suppress(AttributeError): + _obj.__module__ = 'bidict' + + +# * Code review nav * +# ============================================================================ +# Current: __init__.py Next: _abc.py → +# ============================================================================ diff --git a/tapdown/lib/python3.11/site-packages/bidict/_abc.py b/tapdown/lib/python3.11/site-packages/bidict/_abc.py new file mode 100644 index 0000000..d4a30aa --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/_abc.py @@ -0,0 +1,79 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +# * Code review nav * +# (see comments in __init__.py) +# ============================================================================ +# ← Prev: __init__.py Current: _abc.py Next: _base.py → +# ============================================================================ + + +"""Provide the :class:`BidirectionalMapping` abstract base class.""" + +from __future__ import annotations + +import typing as t +from abc import abstractmethod + +from ._typing import KT +from ._typing import VT + + +class BidirectionalMapping(t.Mapping[KT, VT]): + """Abstract base class for bidirectional mapping types. + + Extends :class:`collections.abc.Mapping` primarily by adding the + (abstract) :attr:`inverse` property, + which implementers of :class:`BidirectionalMapping` + should override to return a reference to the inverse + :class:`BidirectionalMapping` instance. + """ + + __slots__ = () + + @property + @abstractmethod + def inverse(self) -> BidirectionalMapping[VT, KT]: + """The inverse of this bidirectional mapping instance. + + *See also* :attr:`bidict.BidictBase.inverse`, :attr:`bidict.BidictBase.inv` + + :raises NotImplementedError: Meant to be overridden in subclasses. + """ + # The @abstractmethod decorator prevents subclasses from being instantiated unless they + # override this method. But an overriding implementation may merely return super().inverse, + # in which case this implementation is used. Raise NotImplementedError to indicate that + # subclasses must actually provide their own implementation. + raise NotImplementedError + + def __inverted__(self) -> t.Iterator[tuple[VT, KT]]: + """Get an iterator over the items in :attr:`inverse`. + + This is functionally equivalent to iterating over the items in the + forward mapping and inverting each one on the fly, but this provides a + more efficient implementation: Assuming the already-inverted items + are stored in :attr:`inverse`, just return an iterator over them directly. + + Providing this default implementation enables external functions, + particularly :func:`~bidict.inverted`, to use this optimized + implementation when available, instead of having to invert on the fly. + + *See also* :func:`bidict.inverted` + """ + return iter(self.inverse.items()) + + +class MutableBidirectionalMapping(BidirectionalMapping[KT, VT], t.MutableMapping[KT, VT]): + """Abstract base class for mutable bidirectional mapping types.""" + + __slots__ = () + + +# * Code review nav * +# ============================================================================ +# ← Prev: __init__.py Current: _abc.py Next: _base.py → +# ============================================================================ diff --git a/tapdown/lib/python3.11/site-packages/bidict/_base.py b/tapdown/lib/python3.11/site-packages/bidict/_base.py new file mode 100644 index 0000000..848a376 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/_base.py @@ -0,0 +1,556 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +# * Code review nav * +# (see comments in __init__.py) +# ============================================================================ +# ← Prev: _abc.py Current: _base.py Next: _frozen.py → +# ============================================================================ + + +"""Provide :class:`BidictBase`.""" + +from __future__ import annotations + +import typing as t +import weakref +from itertools import starmap +from operator import eq +from types import MappingProxyType + +from ._abc import BidirectionalMapping +from ._dup import DROP_NEW +from ._dup import DROP_OLD +from ._dup import ON_DUP_DEFAULT +from ._dup import RAISE +from ._dup import OnDup +from ._exc import DuplicationError +from ._exc import KeyAndValueDuplicationError +from ._exc import KeyDuplicationError +from ._exc import ValueDuplicationError +from ._iter import inverted +from ._iter import iteritems +from ._typing import KT +from ._typing import MISSING +from ._typing import OKT +from ._typing import OVT +from ._typing import VT +from ._typing import Maplike +from ._typing import MapOrItems + + +OldKV = t.Tuple[OKT[KT], OVT[VT]] +DedupResult = t.Optional[OldKV[KT, VT]] +Unwrites = t.List[t.Tuple[t.Any, ...]] +BT = t.TypeVar('BT', bound='BidictBase[t.Any, t.Any]') + + +class BidictKeysView(t.KeysView[KT], t.ValuesView[KT]): + """Since the keys of a bidict are the values of its inverse (and vice versa), + the :class:`~collections.abc.ValuesView` result of calling *bi.values()* + is also a :class:`~collections.abc.KeysView` of *bi.inverse*. + """ + + +class BidictBase(BidirectionalMapping[KT, VT]): + """Base class implementing :class:`BidirectionalMapping`.""" + + #: The default :class:`~bidict.OnDup` + #: that governs behavior when a provided item + #: duplicates the key or value of other item(s). + #: + #: *See also* + #: :ref:`basic-usage:Values Must Be Unique` (https://bidict.rtfd.io/basic-usage.html#values-must-be-unique), + #: :doc:`extending` (https://bidict.rtfd.io/extending.html) + on_dup = ON_DUP_DEFAULT + + _fwdm: t.MutableMapping[KT, VT] #: the backing forward mapping (*key* → *val*) + _invm: t.MutableMapping[VT, KT] #: the backing inverse mapping (*val* → *key*) + + # Use Any rather than KT/VT in the following to avoid "ClassVar cannot contain type variables" errors: + _fwdm_cls: t.ClassVar[type[t.MutableMapping[t.Any, t.Any]]] = dict #: class of the backing forward mapping + _invm_cls: t.ClassVar[type[t.MutableMapping[t.Any, t.Any]]] = dict #: class of the backing inverse mapping + + #: The class of the inverse bidict instance. + _inv_cls: t.ClassVar[type[BidictBase[t.Any, t.Any]]] + + def __init_subclass__(cls) -> None: + super().__init_subclass__() + cls._init_class() + + @classmethod + def _init_class(cls) -> None: + cls._ensure_inv_cls() + cls._set_reversed() + + __reversed__: t.ClassVar[t.Any] + + @classmethod + def _set_reversed(cls) -> None: + """Set __reversed__ for subclasses that do not set it explicitly + according to whether backing mappings are reversible. + """ + if cls is not BidictBase: + resolved = cls.__reversed__ + overridden = resolved is not BidictBase.__reversed__ + if overridden: # E.g. OrderedBidictBase, OrderedBidict + return + backing_reversible = all(issubclass(i, t.Reversible) for i in (cls._fwdm_cls, cls._invm_cls)) + cls.__reversed__ = _fwdm_reversed if backing_reversible else None + + @classmethod + def _ensure_inv_cls(cls) -> None: + """Ensure :attr:`_inv_cls` is set, computing it dynamically if necessary. + + All subclasses provided in :mod:`bidict` are their own inverse classes, + i.e., their backing forward and inverse mappings are both the same type, + but users may define subclasses where this is not the case. + This method ensures that the inverse class is computed correctly regardless. + + See: :ref:`extending:Dynamic Inverse Class Generation` + (https://bidict.rtfd.io/extending.html#dynamic-inverse-class-generation) + """ + # This _ensure_inv_cls() method is (indirectly) corecursive with _make_inv_cls() below + # in the case that we need to dynamically generate the inverse class: + # 1. _ensure_inv_cls() calls cls._make_inv_cls() + # 2. cls._make_inv_cls() calls type(..., (cls, ...), ...) to dynamically generate inv_cls + # 3. Our __init_subclass__ hook (see above) is automatically called on inv_cls + # 4. inv_cls.__init_subclass__() calls inv_cls._ensure_inv_cls() + # 5. inv_cls._ensure_inv_cls() resolves to this implementation + # (inv_cls deliberately does not override this), so we're back where we started. + # But since the _make_inv_cls() call will have set inv_cls.__dict__._inv_cls, + # just check if it's already set before calling _make_inv_cls() to prevent infinite recursion. + if getattr(cls, '__dict__', {}).get('_inv_cls'): # Don't assume cls.__dict__ (e.g. mypyc native class) + return + cls._inv_cls = cls._make_inv_cls() + + @classmethod + def _make_inv_cls(cls: type[BT]) -> type[BT]: + diff = cls._inv_cls_dict_diff() + cls_is_own_inv = all(getattr(cls, k, MISSING) == v for (k, v) in diff.items()) + if cls_is_own_inv: + return cls + # Suppress auto-calculation of _inv_cls's _inv_cls since we know it already. + # Works with the guard in BidictBase._ensure_inv_cls() to prevent infinite recursion. + diff['_inv_cls'] = cls + inv_cls = type(f'{cls.__name__}Inv', (cls, GeneratedBidictInverse), diff) + inv_cls.__module__ = cls.__module__ + return t.cast(t.Type[BT], inv_cls) + + @classmethod + def _inv_cls_dict_diff(cls) -> dict[str, t.Any]: + return { + '_fwdm_cls': cls._invm_cls, + '_invm_cls': cls._fwdm_cls, + } + + def __init__(self, arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> None: + """Make a new bidirectional mapping. + The signature behaves like that of :class:`dict`. + ktems passed via positional arg are processed first, + followed by any items passed via keyword argument. + Any duplication encountered along the way + is handled as per :attr:`on_dup`. + """ + self._fwdm = self._fwdm_cls() + self._invm = self._invm_cls() + self._update(arg, kw, rollback=False) + + # If Python ever adds support for higher-kinded types, `inverse` could use them, e.g. + # def inverse(self: BT[KT, VT]) -> BT[VT, KT]: + # Ref: https://github.com/python/typing/issues/548#issuecomment-621571821 + @property + def inverse(self) -> BidictBase[VT, KT]: + """The inverse of this bidirectional mapping instance.""" + # When `bi.inverse` is called for the first time, this method + # computes the inverse instance, stores it for subsequent use, and then + # returns it. It also stores a reference on `bi.inverse` back to `bi`, + # but uses a weakref to avoid creating a reference cycle. Strong references + # to inverse instances are stored in ._inv, and weak references are stored + # in ._invweak. + + # First check if a strong reference is already stored. + inv: BidictBase[VT, KT] | None = getattr(self, '_inv', None) + if inv is not None: + return inv + # Next check if a weak reference is already stored. + invweak = getattr(self, '_invweak', None) + if invweak is not None: + inv = invweak() # Try to resolve a strong reference and return it. + if inv is not None: + return inv + # No luck. Compute the inverse reference and store it for subsequent use. + inv = self._make_inverse() + self._inv: BidictBase[VT, KT] | None = inv + self._invweak: weakref.ReferenceType[BidictBase[VT, KT]] | None = None + # Also store a weak reference back to `instance` on its inverse instance, so that + # the second `.inverse` access in `bi.inverse.inverse` hits the cached weakref. + inv._inv = None + inv._invweak = weakref.ref(self) + # In e.g. `bidict().inverse.inverse`, this design ensures that a strong reference + # back to the original instance is retained before its refcount drops to zero, + # avoiding an unintended potential deallocation. + return inv + + def _make_inverse(self) -> BidictBase[VT, KT]: + inv: BidictBase[VT, KT] = self._inv_cls() + inv._fwdm = self._invm + inv._invm = self._fwdm + return inv + + @property + def inv(self) -> BidictBase[VT, KT]: + """Alias for :attr:`inverse`.""" + return self.inverse + + def __repr__(self) -> str: + """See :func:`repr`.""" + clsname = self.__class__.__name__ + items = dict(self.items()) if self else '' + return f'{clsname}({items})' + + def values(self) -> BidictKeysView[VT]: + """A set-like object providing a view on the contained values. + + Since the values of a bidict are equivalent to the keys of its inverse, + this method returns a set-like object for this bidict's values + rather than just a collections.abc.ValuesView. + This object supports set operations like union and difference, + and constant- rather than linear-time containment checks, + and is no more expensive to provide than the less capable + collections.abc.ValuesView would be. + + See :meth:`keys` for more information. + """ + return t.cast(BidictKeysView[VT], self.inverse.keys()) + + def keys(self) -> t.KeysView[KT]: + """A set-like object providing a view on the contained keys. + + When *b._fwdm* is a :class:`dict`, *b.keys()* returns a + *dict_keys* object that behaves exactly the same as + *collections.abc.KeysView(b)*, except for + + - offering better performance + + - being reversible on Python 3.8+ + + - having a .mapping attribute in Python 3.10+ + that exposes a mappingproxy to *b._fwdm*. + """ + fwdm, fwdm_cls = self._fwdm, self._fwdm_cls + return fwdm.keys() if fwdm_cls is dict else BidictKeysView(self) + + def items(self) -> t.ItemsView[KT, VT]: + """A set-like object providing a view on the contained items. + + When *b._fwdm* is a :class:`dict`, *b.items()* returns a + *dict_items* object that behaves exactly the same as + *collections.abc.ItemsView(b)*, except for: + + - offering better performance + + - being reversible on Python 3.8+ + + - having a .mapping attribute in Python 3.10+ + that exposes a mappingproxy to *b._fwdm*. + """ + return self._fwdm.items() if self._fwdm_cls is dict else super().items() + + # The inherited collections.abc.Mapping.__contains__() method is implemented by doing a `try` + # `except KeyError` around `self[key]`. The following implementation is much faster, + # especially in the missing case. + def __contains__(self, key: t.Any) -> bool: + """True if the mapping contains the specified key, else False.""" + return key in self._fwdm + + # The inherited collections.abc.Mapping.__eq__() method is implemented in terms of an inefficient + # `dict(self.items()) == dict(other.items())` comparison, so override it with a + # more efficient implementation. + def __eq__(self, other: object) -> bool: + """*x.__eq__(other) ⟺ x == other* + + Equivalent to *dict(x.items()) == dict(other.items())* + but more efficient. + + Note that :meth:`bidict's __eq__() ` implementation + is inherited by subclasses, + in particular by the ordered bidict subclasses, + so even with ordered bidicts, + :ref:`== comparison is order-insensitive ` + (https://bidict.rtfd.io/other-bidict-types.html#eq-is-order-insensitive). + + *See also* :meth:`equals_order_sensitive` + """ + if isinstance(other, t.Mapping): + return self._fwdm.items() == other.items() + # Ref: https://docs.python.org/3/library/constants.html#NotImplemented + return NotImplemented + + def equals_order_sensitive(self, other: object) -> bool: + """Order-sensitive equality check. + + *See also* :ref:`eq-order-insensitive` + (https://bidict.rtfd.io/other-bidict-types.html#eq-is-order-insensitive) + """ + if not isinstance(other, t.Mapping) or len(self) != len(other): + return False + return all(starmap(eq, zip(self.items(), other.items()))) + + def _dedup(self, key: KT, val: VT, on_dup: OnDup) -> DedupResult[KT, VT]: + """Check *key* and *val* for any duplication in self. + + Handle any duplication as per the passed in *on_dup*. + + If (key, val) is already present, return None + since writing (key, val) would be a no-op. + + If duplication is found and the corresponding :class:`~bidict.OnDupAction` is + :attr:`~bidict.DROP_NEW`, return None. + + If duplication is found and the corresponding :class:`~bidict.OnDupAction` is + :attr:`~bidict.RAISE`, raise the appropriate exception. + + If duplication is found and the corresponding :class:`~bidict.OnDupAction` is + :attr:`~bidict.DROP_OLD`, or if no duplication is found, + return *(oldkey, oldval)*. + """ + fwdm, invm = self._fwdm, self._invm + oldval: OVT[VT] = fwdm.get(key, MISSING) + oldkey: OKT[KT] = invm.get(val, MISSING) + isdupkey, isdupval = oldval is not MISSING, oldkey is not MISSING + if isdupkey and isdupval: + if key == oldkey: + assert val == oldval + # (key, val) duplicates an existing item -> no-op. + return None + # key and val each duplicate a different existing item. + if on_dup.val is RAISE: + raise KeyAndValueDuplicationError(key, val) + if on_dup.val is DROP_NEW: + return None + assert on_dup.val is DROP_OLD + # Fall through to the return statement on the last line. + elif isdupkey: + if on_dup.key is RAISE: + raise KeyDuplicationError(key) + if on_dup.key is DROP_NEW: + return None + assert on_dup.key is DROP_OLD + # Fall through to the return statement on the last line. + elif isdupval: + if on_dup.val is RAISE: + raise ValueDuplicationError(val) + if on_dup.val is DROP_NEW: + return None + assert on_dup.val is DROP_OLD + # Fall through to the return statement on the last line. + # else neither isdupkey nor isdupval. + return oldkey, oldval + + def _write(self, newkey: KT, newval: VT, oldkey: OKT[KT], oldval: OVT[VT], unwrites: Unwrites | None) -> None: + """Insert (newkey, newval), extending *unwrites* with associated inverse operations if provided. + + *oldkey* and *oldval* are as returned by :meth:`_dedup`. + + If *unwrites* is not None, it is extended with the inverse operations necessary to undo the write. + This design allows :meth:`_update` to roll back a partially applied update that fails part-way through + when necessary. + + This design also allows subclasses that require additional operations to easily extend this implementation. + For example, :class:`bidict.OrderedBidictBase` calls this inherited implementation, and then extends *unwrites* + with additional operations needed to keep its internal linked list nodes consistent with its items' order + as changes are made. + """ + fwdm, invm = self._fwdm, self._invm + fwdm_set, invm_set = fwdm.__setitem__, invm.__setitem__ + fwdm_del, invm_del = fwdm.__delitem__, invm.__delitem__ + # Always perform the following writes regardless of duplication. + fwdm_set(newkey, newval) + invm_set(newval, newkey) + if oldval is MISSING and oldkey is MISSING: # no key or value duplication + # {0: 1, 2: 3} | {4: 5} => {0: 1, 2: 3, 4: 5} + if unwrites is not None: + unwrites.extend(( + (fwdm_del, newkey), + (invm_del, newval), + )) + elif oldval is not MISSING and oldkey is not MISSING: # key and value duplication across two different items + # {0: 1, 2: 3} | {0: 3} => {0: 3} + fwdm_del(oldkey) + invm_del(oldval) + if unwrites is not None: + unwrites.extend(( + (fwdm_set, newkey, oldval), + (invm_set, oldval, newkey), + (fwdm_set, oldkey, newval), + (invm_set, newval, oldkey), + )) + elif oldval is not MISSING: # just key duplication + # {0: 1, 2: 3} | {2: 4} => {0: 1, 2: 4} + invm_del(oldval) + if unwrites is not None: + unwrites.extend(( + (fwdm_set, newkey, oldval), + (invm_set, oldval, newkey), + (invm_del, newval), + )) + else: + assert oldkey is not MISSING # just value duplication + # {0: 1, 2: 3} | {4: 3} => {0: 1, 4: 3} + fwdm_del(oldkey) + if unwrites is not None: + unwrites.extend(( + (fwdm_set, oldkey, newval), + (invm_set, newval, oldkey), + (fwdm_del, newkey), + )) + + def _update( + self, + arg: MapOrItems[KT, VT], + kw: t.Mapping[str, VT] = MappingProxyType({}), + *, + rollback: bool | None = None, + on_dup: OnDup | None = None, + ) -> None: + """Update with the items from *arg* and *kw*, maybe failing and rolling back as per *on_dup* and *rollback*.""" + # Note: We must process input in a single pass, since arg may be a generator. + if not isinstance(arg, (t.Iterable, Maplike)): + raise TypeError(f"'{arg.__class__.__name__}' object is not iterable") + if not arg and not kw: + return + if on_dup is None: + on_dup = self.on_dup + if rollback is None: + rollback = RAISE in on_dup + + # Fast path when we're empty and updating only from another bidict (i.e. no dup vals in new items). + if not self and not kw and isinstance(arg, BidictBase): + self._init_from(arg) + return + + # Fast path when we're adding more items than we contain already and rollback is enabled: + # Update a copy of self with rollback disabled. Fail if that fails, otherwise become the copy. + if rollback and isinstance(arg, t.Sized) and len(arg) + len(kw) > len(self): + tmp = self.copy() + tmp._update(arg, kw, rollback=False, on_dup=on_dup) + self._init_from(tmp) + return + + # In all other cases, benchmarking has indicated that the update is best implemented as follows: + # For each new item, perform a dup check (raising if necessary), and apply the associated writes we need to + # perform on our backing _fwdm and _invm mappings. If rollback is enabled, also compute the associated unwrites + # as we go. If the update results in a DuplicationError and rollback is enabled, apply the accumulated unwrites + # before raising, to ensure that we fail clean. + write = self._write + unwrites: Unwrites | None = [] if rollback else None + for key, val in iteritems(arg, **kw): + try: + dedup_result = self._dedup(key, val, on_dup) + except DuplicationError: + if unwrites is not None: + for fn, *args in reversed(unwrites): + fn(*args) + raise + if dedup_result is not None: + write(key, val, *dedup_result, unwrites=unwrites) + + def __copy__(self: BT) -> BT: + """Used for the copy protocol. See the :mod:`copy` module.""" + return self.copy() + + def copy(self: BT) -> BT: + """Make a (shallow) copy of this bidict.""" + # Could just `return self.__class__(self)` here, but the below is faster. The former + # would copy this bidict's items into a new instance one at a time (checking for duplication + # for each item), whereas the below copies from the backing mappings all at once, and foregoes + # item-by-item duplication checking since the backing mappings have been checked already. + return self._from_other(self.__class__, self) + + @staticmethod + def _from_other(bt: type[BT], other: MapOrItems[KT, VT], inv: bool = False) -> BT: + """Fast, private constructor based on :meth:`_init_from`. + + If *inv* is true, return the inverse of the instance instead of the instance itself. + (Useful for pickling with dynamically-generated inverse classes -- see :meth:`__reduce__`.) + """ + inst = bt() + inst._init_from(other) + return t.cast(BT, inst.inverse) if inv else inst + + def _init_from(self, other: MapOrItems[KT, VT]) -> None: + """Fast init from *other*, bypassing item-by-item duplication checking.""" + self._fwdm.clear() + self._invm.clear() + self._fwdm.update(other) + # If other is a bidict, use its existing backing inverse mapping, otherwise + # other could be a generator that's now exhausted, so invert self._fwdm on the fly. + inv = other.inverse if isinstance(other, BidictBase) else inverted(self._fwdm) + self._invm.update(inv) + + # other's type is Mapping rather than Maplike since bidict() | SupportsKeysAndGetItem({}) + # raises a TypeError, just like dict() | SupportsKeysAndGetItem({}) does. + def __or__(self: BT, other: t.Mapping[KT, VT]) -> BT: + """Return self|other.""" + if not isinstance(other, t.Mapping): + return NotImplemented + new = self.copy() + new._update(other, rollback=False) + return new + + def __ror__(self: BT, other: t.Mapping[KT, VT]) -> BT: + """Return other|self.""" + if not isinstance(other, t.Mapping): + return NotImplemented + new = self.__class__(other) + new._update(self, rollback=False) + return new + + def __len__(self) -> int: + """The number of contained items.""" + return len(self._fwdm) + + def __iter__(self) -> t.Iterator[KT]: + """Iterator over the contained keys.""" + return iter(self._fwdm) + + def __getitem__(self, key: KT) -> VT: + """*x.__getitem__(key) ⟺ x[key]*""" + return self._fwdm[key] + + def __reduce__(self) -> tuple[t.Any, ...]: + """Return state information for pickling.""" + cls = self.__class__ + inst: t.Mapping[t.Any, t.Any] = self + # If this bidict's class is dynamically generated, pickle the inverse instead, whose (presumably not + # dynamically generated) class the caller is more likely to have a reference to somewhere in sys.modules + # that pickle can discover. + if should_invert := isinstance(self, GeneratedBidictInverse): + cls = self._inv_cls + inst = self.inverse + return self._from_other, (cls, dict(inst), should_invert) + + +# See BidictBase._set_reversed() above. +def _fwdm_reversed(self: BidictBase[KT, t.Any]) -> t.Iterator[KT]: + """Iterator over the contained keys in reverse order.""" + assert isinstance(self._fwdm, t.Reversible) + return reversed(self._fwdm) + + +BidictBase._init_class() + + +class GeneratedBidictInverse: + """Base class for dynamically-generated inverse bidict classes.""" + + +# * Code review nav * +# ============================================================================ +# ← Prev: _abc.py Current: _base.py Next: _frozen.py → +# ============================================================================ diff --git a/tapdown/lib/python3.11/site-packages/bidict/_bidict.py b/tapdown/lib/python3.11/site-packages/bidict/_bidict.py new file mode 100644 index 0000000..94dd3db --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/_bidict.py @@ -0,0 +1,194 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +# * Code review nav * +# (see comments in __init__.py) +# ============================================================================ +# ← Prev: _frozen.py Current: _bidict.py Next: _orderedbase.py → +# ============================================================================ + + +"""Provide :class:`MutableBidict` and :class:`bidict`.""" + +from __future__ import annotations + +import typing as t + +from ._abc import MutableBidirectionalMapping +from ._base import BidictBase +from ._dup import ON_DUP_DROP_OLD +from ._dup import ON_DUP_RAISE +from ._dup import OnDup +from ._typing import DT +from ._typing import KT +from ._typing import MISSING +from ._typing import ODT +from ._typing import VT +from ._typing import MapOrItems + + +class MutableBidict(BidictBase[KT, VT], MutableBidirectionalMapping[KT, VT]): + """Base class for mutable bidirectional mappings.""" + + if t.TYPE_CHECKING: + + @property + def inverse(self) -> MutableBidict[VT, KT]: ... + + @property + def inv(self) -> MutableBidict[VT, KT]: ... + + def _pop(self, key: KT) -> VT: + val = self._fwdm.pop(key) + del self._invm[val] + return val + + def __delitem__(self, key: KT) -> None: + """*x.__delitem__(y) ⟺ del x[y]*""" + self._pop(key) + + def __setitem__(self, key: KT, val: VT) -> None: + """Set the value for *key* to *val*. + + If *key* is already associated with *val*, this is a no-op. + + If *key* is already associated with a different value, + the old value will be replaced with *val*, + as with dict's :meth:`__setitem__`. + + If *val* is already associated with a different key, + an exception is raised + to protect against accidental removal of the key + that's currently associated with *val*. + + Use :meth:`put` instead if you want to specify different behavior in + the case that the provided key or value duplicates an existing one. + Or use :meth:`forceput` to unconditionally associate *key* with *val*, + replacing any existing items as necessary to preserve uniqueness. + + :raises bidict.ValueDuplicationError: if *val* duplicates that of an + existing item. + + :raises bidict.KeyAndValueDuplicationError: if *key* duplicates the key of an + existing item and *val* duplicates the value of a different + existing item. + """ + self.put(key, val, on_dup=self.on_dup) + + def put(self, key: KT, val: VT, on_dup: OnDup = ON_DUP_RAISE) -> None: + """Associate *key* with *val*, honoring the :class:`OnDup` given in *on_dup*. + + For example, if *on_dup* is :attr:`~bidict.ON_DUP_RAISE`, + then *key* will be associated with *val* if and only if + *key* is not already associated with an existing value and + *val* is not already associated with an existing key, + otherwise an exception will be raised. + + If *key* is already associated with *val*, this is a no-op. + + :raises bidict.KeyDuplicationError: if attempting to insert an item + whose key only duplicates an existing item's, and *on_dup.key* is + :attr:`~bidict.RAISE`. + + :raises bidict.ValueDuplicationError: if attempting to insert an item + whose value only duplicates an existing item's, and *on_dup.val* is + :attr:`~bidict.RAISE`. + + :raises bidict.KeyAndValueDuplicationError: if attempting to insert an + item whose key duplicates one existing item's, and whose value + duplicates another existing item's, and *on_dup.val* is + :attr:`~bidict.RAISE`. + """ + self._update(((key, val),), on_dup=on_dup) + + def forceput(self, key: KT, val: VT) -> None: + """Associate *key* with *val* unconditionally. + + Replace any existing mappings containing key *key* or value *val* + as necessary to preserve uniqueness. + """ + self.put(key, val, on_dup=ON_DUP_DROP_OLD) + + def clear(self) -> None: + """Remove all items.""" + self._fwdm.clear() + self._invm.clear() + + @t.overload + def pop(self, key: KT, /) -> VT: ... + @t.overload + def pop(self, key: KT, default: DT = ..., /) -> VT | DT: ... + + def pop(self, key: KT, default: ODT[DT] = MISSING, /) -> VT | DT: + """*x.pop(k[, d]) → v* + + Remove specified key and return the corresponding value. + + :raises KeyError: if *key* is not found and no *default* is provided. + """ + try: + return self._pop(key) + except KeyError: + if default is MISSING: + raise + return default + + def popitem(self) -> tuple[KT, VT]: + """*x.popitem() → (k, v)* + + Remove and return some item as a (key, value) pair. + + :raises KeyError: if *x* is empty. + """ + key, val = self._fwdm.popitem() + del self._invm[val] + return key, val + + def update(self, arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> None: + """Like calling :meth:`putall` with *self.on_dup* passed for *on_dup*.""" + self._update(arg, kw=kw) + + def forceupdate(self, arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> None: + """Like a bulk :meth:`forceput`.""" + self._update(arg, kw=kw, on_dup=ON_DUP_DROP_OLD) + + def putall(self, items: MapOrItems[KT, VT], on_dup: OnDup = ON_DUP_RAISE) -> None: + """Like a bulk :meth:`put`. + + If one of the given items causes an exception to be raised, + none of the items is inserted. + """ + self._update(items, on_dup=on_dup) + + # other's type is Mapping rather than Maplike since bidict() |= SupportsKeysAndGetItem({}) + # raises a TypeError, just like dict() |= SupportsKeysAndGetItem({}) does. + def __ior__(self, other: t.Mapping[KT, VT]) -> MutableBidict[KT, VT]: + """Return self|=other.""" + self.update(other) + return self + + +class bidict(MutableBidict[KT, VT]): + """The main bidirectional mapping type. + + See :ref:`intro:Introduction` and :ref:`basic-usage:Basic Usage` + to get started (also available at https://bidict.rtfd.io). + """ + + if t.TYPE_CHECKING: + + @property + def inverse(self) -> bidict[VT, KT]: ... + + @property + def inv(self) -> bidict[VT, KT]: ... + + +# * Code review nav * +# ============================================================================ +# ← Prev: _frozen.py Current: _bidict.py Next: _orderedbase.py → +# ============================================================================ diff --git a/tapdown/lib/python3.11/site-packages/bidict/_dup.py b/tapdown/lib/python3.11/site-packages/bidict/_dup.py new file mode 100644 index 0000000..fd25b61 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/_dup.py @@ -0,0 +1,61 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Provide :class:`OnDup` and related functionality.""" + +from __future__ import annotations + +import typing as t +from enum import Enum + + +class OnDupAction(Enum): + """An action to take to prevent duplication from occurring.""" + + #: Raise a :class:`~bidict.DuplicationError`. + RAISE = 'RAISE' + #: Overwrite existing items with new items. + DROP_OLD = 'DROP_OLD' + #: Keep existing items and drop new items. + DROP_NEW = 'DROP_NEW' + + def __repr__(self) -> str: + return f'{self.__class__.__name__}.{self.name}' + + +RAISE: t.Final[OnDupAction] = OnDupAction.RAISE +DROP_OLD: t.Final[OnDupAction] = OnDupAction.DROP_OLD +DROP_NEW: t.Final[OnDupAction] = OnDupAction.DROP_NEW + + +class OnDup(t.NamedTuple): + r"""A combination of :class:`~bidict.OnDupAction`\s specifying how to handle various types of duplication. + + The :attr:`~OnDup.key` field specifies what action to take when a duplicate key is encountered. + + The :attr:`~OnDup.val` field specifies what action to take when a duplicate value is encountered. + + In the case of both key and value duplication across two different items, + only :attr:`~OnDup.val` is used. + + *See also* :ref:`basic-usage:Values Must Be Unique` + (https://bidict.rtfd.io/basic-usage.html#values-must-be-unique) + """ + + key: OnDupAction = DROP_OLD + val: OnDupAction = RAISE + + +#: Default :class:`OnDup` used for the +#: :meth:`~bidict.bidict.__init__`, +#: :meth:`~bidict.bidict.__setitem__`, and +#: :meth:`~bidict.bidict.update` methods. +ON_DUP_DEFAULT: t.Final[OnDup] = OnDup(key=DROP_OLD, val=RAISE) +#: An :class:`OnDup` whose members are all :obj:`RAISE`. +ON_DUP_RAISE: t.Final[OnDup] = OnDup(key=RAISE, val=RAISE) +#: An :class:`OnDup` whose members are all :obj:`DROP_OLD`. +ON_DUP_DROP_OLD: t.Final[OnDup] = OnDup(key=DROP_OLD, val=DROP_OLD) diff --git a/tapdown/lib/python3.11/site-packages/bidict/_exc.py b/tapdown/lib/python3.11/site-packages/bidict/_exc.py new file mode 100644 index 0000000..e2a96f3 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/_exc.py @@ -0,0 +1,36 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Provide all bidict exceptions.""" + +from __future__ import annotations + + +class BidictException(Exception): + """Base class for bidict exceptions.""" + + +class DuplicationError(BidictException): + """Base class for exceptions raised when uniqueness is violated + as per the :attr:`~bidict.RAISE` :class:`~bidict.OnDupAction`. + """ + + +class KeyDuplicationError(DuplicationError): + """Raised when a given key is not unique.""" + + +class ValueDuplicationError(DuplicationError): + """Raised when a given value is not unique.""" + + +class KeyAndValueDuplicationError(KeyDuplicationError, ValueDuplicationError): + """Raised when a given item's key and value are not unique. + + That is, its key duplicates that of another item, + and its value duplicates that of a different other item. + """ diff --git a/tapdown/lib/python3.11/site-packages/bidict/_frozen.py b/tapdown/lib/python3.11/site-packages/bidict/_frozen.py new file mode 100644 index 0000000..e2f789d --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/_frozen.py @@ -0,0 +1,50 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +# * Code review nav * +# (see comments in __init__.py) +# ============================================================================ +# ← Prev: _base.py Current: _frozen.py Next: _bidict.py → +# ============================================================================ + +"""Provide :class:`frozenbidict`, an immutable, hashable bidirectional mapping type.""" + +from __future__ import annotations + +import typing as t + +from ._base import BidictBase +from ._typing import KT +from ._typing import VT + + +class frozenbidict(BidictBase[KT, VT]): + """Immutable, hashable bidict type.""" + + _hash: int + + if t.TYPE_CHECKING: + + @property + def inverse(self) -> frozenbidict[VT, KT]: ... + + @property + def inv(self) -> frozenbidict[VT, KT]: ... + + def __hash__(self) -> int: + """The hash of this bidict as determined by its items.""" + if getattr(self, '_hash', None) is None: + # The following is like hash(frozenset(self.items())) + # but more memory efficient. See also: https://bugs.python.org/issue46684 + self._hash = t.ItemsView(self)._hash() + return self._hash + + +# * Code review nav * +# ============================================================================ +# ← Prev: _base.py Current: _frozen.py Next: _bidict.py → +# ============================================================================ diff --git a/tapdown/lib/python3.11/site-packages/bidict/_iter.py b/tapdown/lib/python3.11/site-packages/bidict/_iter.py new file mode 100644 index 0000000..53ad25d --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/_iter.py @@ -0,0 +1,51 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Functions for iterating over items in a mapping.""" + +from __future__ import annotations + +import typing as t +from operator import itemgetter + +from ._typing import KT +from ._typing import VT +from ._typing import ItemsIter +from ._typing import Maplike +from ._typing import MapOrItems + + +def iteritems(arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> ItemsIter[KT, VT]: + """Yield the items from *arg* and *kw* in the order given.""" + if isinstance(arg, t.Mapping): + yield from arg.items() + elif isinstance(arg, Maplike): + yield from ((k, arg[k]) for k in arg.keys()) + else: + yield from arg + yield from t.cast(ItemsIter[KT, VT], kw.items()) + + +swap: t.Final = itemgetter(1, 0) + + +def inverted(arg: MapOrItems[KT, VT]) -> ItemsIter[VT, KT]: + """Yield the inverse items of the provided object. + + If *arg* has a :func:`callable` ``__inverted__`` attribute, + return the result of calling it. + + Otherwise, return an iterator over the items in `arg`, + inverting each item on the fly. + + *See also* :attr:`bidict.BidirectionalMapping.__inverted__` + """ + invattr = getattr(arg, '__inverted__', None) + if callable(invattr): + inv: ItemsIter[VT, KT] = invattr() + return inv + return map(swap, iteritems(arg)) diff --git a/tapdown/lib/python3.11/site-packages/bidict/_orderedbase.py b/tapdown/lib/python3.11/site-packages/bidict/_orderedbase.py new file mode 100644 index 0000000..92f2633 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/_orderedbase.py @@ -0,0 +1,238 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +# * Code review nav * +# (see comments in __init__.py) +# ============================================================================ +# ← Prev: _bidict.py Current: _orderedbase.py Next: _orderedbidict.py → +# ============================================================================ + + +"""Provide :class:`OrderedBidictBase`.""" + +from __future__ import annotations + +import typing as t +from weakref import ref as weakref + +from ._base import BidictBase +from ._base import Unwrites +from ._bidict import bidict +from ._iter import iteritems +from ._typing import KT +from ._typing import MISSING +from ._typing import OKT +from ._typing import OVT +from ._typing import VT +from ._typing import MapOrItems + + +AT = t.TypeVar('AT') # attr type + + +class WeakAttr(t.Generic[AT]): + """Descriptor to automatically manage (de)referencing the given slot as a weakref. + + See https://docs.python.org/3/howto/descriptor.html#managed-attributes + for an intro to using descriptors like this for managed attributes. + """ + + def __init__(self, *, slot: str) -> None: + self.slot = slot + + def __set__(self, instance: t.Any, value: AT) -> None: + setattr(instance, self.slot, weakref(value)) + + def __get__(self, instance: t.Any, __owner: t.Any = None) -> AT: + return t.cast(AT, getattr(instance, self.slot)()) + + +class Node: + """A node in a circular doubly-linked list + used to encode the order of items in an ordered bidict. + + A weak reference to the previous node is stored + to avoid creating strong reference cycles. + Referencing/dereferencing the weakref is handled automatically by :class:`WeakAttr`. + """ + + prv: WeakAttr[Node] = WeakAttr(slot='_prv_weak') + __slots__ = ('__weakref__', '_prv_weak', 'nxt') + + nxt: Node | WeakAttr[Node] # Allow subclasses to use a WeakAttr for nxt too (see SentinelNode) + + def __init__(self, prv: Node, nxt: Node) -> None: + self.prv = prv + self.nxt = nxt + + def unlink(self) -> None: + """Remove self from in between prv and nxt. + Self's references to prv and nxt are retained so it can be relinked (see below). + """ + self.prv.nxt = self.nxt + self.nxt.prv = self.prv + + def relink(self) -> None: + """Restore self between prv and nxt after unlinking (see above).""" + self.prv.nxt = self.nxt.prv = self + + +class SentinelNode(Node): + """Special node in a circular doubly-linked list + that links the first node with the last node. + When its next and previous references point back to itself + it represents an empty list. + """ + + nxt: WeakAttr[Node] = WeakAttr(slot='_nxt_weak') + __slots__ = ('_nxt_weak',) + + def __init__(self) -> None: + super().__init__(self, self) + + def iternodes(self, *, reverse: bool = False) -> t.Iterator[Node]: + """Iterator yielding nodes in the requested order.""" + attr = 'prv' if reverse else 'nxt' + node = getattr(self, attr) + while node is not self: + yield node + node = getattr(node, attr) + + def new_last_node(self) -> Node: + """Create and return a new terminal node.""" + old_last = self.prv + new_last = Node(old_last, self) + old_last.nxt = self.prv = new_last + return new_last + + +class OrderedBidictBase(BidictBase[KT, VT]): + """Base class implementing an ordered :class:`BidirectionalMapping`.""" + + _node_by_korv: bidict[t.Any, Node] + _bykey: bool + + def __init__(self, arg: MapOrItems[KT, VT] = (), /, **kw: VT) -> None: + """Make a new ordered bidirectional mapping. + The signature behaves like that of :class:`dict`. + Items passed in are added in the order they are passed, + respecting the :attr:`~bidict.BidictBase.on_dup` + class attribute in the process. + + The order in which items are inserted is remembered, + similar to :class:`collections.OrderedDict`. + """ + self._sntl = SentinelNode() + self._node_by_korv = bidict() + self._bykey = True + super().__init__(arg, **kw) + + if t.TYPE_CHECKING: + + @property + def inverse(self) -> OrderedBidictBase[VT, KT]: ... + + @property + def inv(self) -> OrderedBidictBase[VT, KT]: ... + + def _make_inverse(self) -> OrderedBidictBase[VT, KT]: + inv = t.cast(OrderedBidictBase[VT, KT], super()._make_inverse()) + inv._sntl = self._sntl + inv._node_by_korv = self._node_by_korv + inv._bykey = not self._bykey + return inv + + def _assoc_node(self, node: Node, key: KT, val: VT) -> None: + korv = key if self._bykey else val + self._node_by_korv.forceput(korv, node) + + def _dissoc_node(self, node: Node) -> None: + del self._node_by_korv.inverse[node] + node.unlink() + + def _init_from(self, other: MapOrItems[KT, VT]) -> None: + """See :meth:`BidictBase._init_from`.""" + super()._init_from(other) + bykey = self._bykey + korv_by_node = self._node_by_korv.inverse + korv_by_node.clear() + korv_by_node_set = korv_by_node.__setitem__ + self._sntl.nxt = self._sntl.prv = self._sntl + new_node = self._sntl.new_last_node + for k, v in iteritems(other): + korv_by_node_set(new_node(), k if bykey else v) + + def _write(self, newkey: KT, newval: VT, oldkey: OKT[KT], oldval: OVT[VT], unwrites: Unwrites | None) -> None: + """See :meth:`bidict.BidictBase._spec_write`.""" + super()._write(newkey, newval, oldkey, oldval, unwrites) + assoc, dissoc = self._assoc_node, self._dissoc_node + node_by_korv, bykey = self._node_by_korv, self._bykey + if oldval is MISSING and oldkey is MISSING: # no key or value duplication + # {0: 1, 2: 3} | {4: 5} => {0: 1, 2: 3, 4: 5} + newnode = self._sntl.new_last_node() + assoc(newnode, newkey, newval) + if unwrites is not None: + unwrites.append((dissoc, newnode)) + elif oldval is not MISSING and oldkey is not MISSING: # key and value duplication across two different items + # {0: 1, 2: 3} | {0: 3} => {0: 3} + # n1, n2 => n1 (collapse n1 and n2 into n1) + # oldkey: 2, oldval: 1, oldnode: n2, newkey: 0, newval: 3, newnode: n1 + if bykey: + oldnode = node_by_korv[oldkey] + newnode = node_by_korv[newkey] + else: + oldnode = node_by_korv[newval] + newnode = node_by_korv[oldval] + dissoc(oldnode) + assoc(newnode, newkey, newval) + if unwrites is not None: + unwrites.extend(( + (assoc, newnode, newkey, oldval), + (assoc, oldnode, oldkey, newval), + (oldnode.relink,), + )) + elif oldval is not MISSING: # just key duplication + # {0: 1, 2: 3} | {2: 4} => {0: 1, 2: 4} + # oldkey: MISSING, oldval: 3, newkey: 2, newval: 4 + node = node_by_korv[newkey if bykey else oldval] + assoc(node, newkey, newval) + if unwrites is not None: + unwrites.append((assoc, node, newkey, oldval)) + else: + assert oldkey is not MISSING # just value duplication + # {0: 1, 2: 3} | {4: 3} => {0: 1, 4: 3} + # oldkey: 2, oldval: MISSING, newkey: 4, newval: 3 + node = node_by_korv[oldkey if bykey else newval] + assoc(node, newkey, newval) + if unwrites is not None: + unwrites.append((assoc, node, oldkey, newval)) + + def __iter__(self) -> t.Iterator[KT]: + """Iterator over the contained keys in insertion order.""" + return self._iter(reverse=False) + + def __reversed__(self) -> t.Iterator[KT]: + """Iterator over the contained keys in reverse insertion order.""" + return self._iter(reverse=True) + + def _iter(self, *, reverse: bool = False) -> t.Iterator[KT]: + nodes = self._sntl.iternodes(reverse=reverse) + korv_by_node = self._node_by_korv.inverse + if self._bykey: + for node in nodes: + yield korv_by_node[node] + else: + key_by_val = self._invm + for node in nodes: + val = korv_by_node[node] + yield key_by_val[val] + + +# * Code review nav * +# ============================================================================ +# ← Prev: _bidict.py Current: _orderedbase.py Next: _orderedbidict.py → +# ============================================================================ diff --git a/tapdown/lib/python3.11/site-packages/bidict/_orderedbidict.py b/tapdown/lib/python3.11/site-packages/bidict/_orderedbidict.py new file mode 100644 index 0000000..2fb1757 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/_orderedbidict.py @@ -0,0 +1,172 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +# * Code review nav * +# (see comments in __init__.py) +# ============================================================================ +# ← Prev: _orderedbase.py Current: _orderedbidict.py +# ============================================================================ + + +"""Provide :class:`OrderedBidict`.""" + +from __future__ import annotations + +import typing as t +from collections.abc import Set + +from ._base import BidictKeysView +from ._bidict import MutableBidict +from ._orderedbase import OrderedBidictBase +from ._typing import KT +from ._typing import VT + + +class OrderedBidict(OrderedBidictBase[KT, VT], MutableBidict[KT, VT]): + """Mutable bidict type that maintains items in insertion order.""" + + if t.TYPE_CHECKING: + + @property + def inverse(self) -> OrderedBidict[VT, KT]: ... + + @property + def inv(self) -> OrderedBidict[VT, KT]: ... + + def clear(self) -> None: + """Remove all items.""" + super().clear() + self._node_by_korv.clear() + self._sntl.nxt = self._sntl.prv = self._sntl + + def _pop(self, key: KT) -> VT: + val = super()._pop(key) + node = self._node_by_korv[key if self._bykey else val] + self._dissoc_node(node) + return val + + def popitem(self, last: bool = True) -> tuple[KT, VT]: + """*b.popitem() → (k, v)* + + If *last* is true, + remove and return the most recently added item as a (key, value) pair. + Otherwise, remove and return the least recently added item. + + :raises KeyError: if *b* is empty. + """ + if not self: + raise KeyError('OrderedBidict is empty') + node = getattr(self._sntl, 'prv' if last else 'nxt') + korv = self._node_by_korv.inverse[node] + if self._bykey: + return korv, self._pop(korv) + return self.inverse._pop(korv), korv + + def move_to_end(self, key: KT, last: bool = True) -> None: + """Move the item with the given key to the end if *last* is true, else to the beginning. + + :raises KeyError: if *key* is missing + """ + korv = key if self._bykey else self._fwdm[key] + node = self._node_by_korv[korv] + node.prv.nxt = node.nxt + node.nxt.prv = node.prv + sntl = self._sntl + if last: + lastnode = sntl.prv + node.prv = lastnode + node.nxt = sntl + sntl.prv = lastnode.nxt = node + else: + firstnode = sntl.nxt + node.prv = sntl + node.nxt = firstnode + sntl.nxt = firstnode.prv = node + + # Override the keys() and items() implementations inherited from BidictBase, + # which may delegate to the backing _fwdm dict, since this is a mutable ordered bidict, + # and therefore the ordering of items can get out of sync with the backing mappings + # after mutation. (Need not override values() because it delegates to .inverse.keys().) + def keys(self) -> t.KeysView[KT]: + """A set-like object providing a view on the contained keys.""" + return _OrderedBidictKeysView(self) + + def items(self) -> t.ItemsView[KT, VT]: + """A set-like object providing a view on the contained items.""" + return _OrderedBidictItemsView(self) + + +# The following MappingView implementations use the __iter__ implementations +# inherited from their superclass counterparts in collections.abc, so they +# continue to yield items in the correct order even after an OrderedBidict +# is mutated. They also provide a __reversed__ implementation, which is not +# provided by the collections.abc superclasses. +class _OrderedBidictKeysView(BidictKeysView[KT]): + _mapping: OrderedBidict[KT, t.Any] + + def __reversed__(self) -> t.Iterator[KT]: + return reversed(self._mapping) + + +class _OrderedBidictItemsView(t.ItemsView[KT, VT]): + _mapping: OrderedBidict[KT, VT] + + def __reversed__(self) -> t.Iterator[tuple[KT, VT]]: + ob = self._mapping + for key in reversed(ob): + yield key, ob[key] + + +# For better performance, make _OrderedBidictKeysView and _OrderedBidictItemsView delegate +# to backing dicts for the methods they inherit from collections.abc.Set. (Cannot delegate +# for __iter__ and __reversed__ since they are order-sensitive.) See also: https://bugs.python.org/issue46713 +_OView = t.Union[t.Type[_OrderedBidictKeysView[KT]], t.Type[_OrderedBidictItemsView[KT, t.Any]]] +_setmethodnames: t.Iterable[str] = ( + '__lt__ __le__ __gt__ __ge__ __eq__ __ne__ __sub__ __rsub__ ' + '__or__ __ror__ __xor__ __rxor__ __and__ __rand__ isdisjoint' +).split() + + +def _override_set_methods_to_use_backing_dict(cls: _OView[KT], viewname: str) -> None: + def make_proxy_method(methodname: str) -> t.Any: + def method(self: _OrderedBidictKeysView[KT] | _OrderedBidictItemsView[KT, t.Any], *args: t.Any) -> t.Any: + fwdm = self._mapping._fwdm + if not isinstance(fwdm, dict): # dict view speedup not available, fall back to Set's implementation. + return getattr(Set, methodname)(self, *args) + fwdm_dict_view = getattr(fwdm, viewname)() + fwdm_dict_view_method = getattr(fwdm_dict_view, methodname) + if ( + len(args) != 1 + or not isinstance((arg := args[0]), self.__class__) + or not isinstance(arg._mapping._fwdm, dict) + ): + return fwdm_dict_view_method(*args) + # self and arg are both _OrderedBidictKeysViews or _OrderedBidictItemsViews whose bidicts are backed by + # a dict. Use arg's backing dict's corresponding view instead of arg. Otherwise, e.g. `ob1.keys() + # < ob2.keys()` would give "TypeError: '<' not supported between instances of '_OrderedBidictKeysView' and + # '_OrderedBidictKeysView'", because both `dict_keys(ob1).__lt__(ob2.keys()) is NotImplemented` and + # `dict_keys(ob2).__gt__(ob1.keys()) is NotImplemented`. + arg_dict = arg._mapping._fwdm + arg_dict_view = getattr(arg_dict, viewname)() + return fwdm_dict_view_method(arg_dict_view) + + method.__name__ = methodname + method.__qualname__ = f'{cls.__qualname__}.{methodname}' + return method + + for name in _setmethodnames: + setattr(cls, name, make_proxy_method(name)) + + +_override_set_methods_to_use_backing_dict(_OrderedBidictKeysView, 'keys') +_override_set_methods_to_use_backing_dict(_OrderedBidictItemsView, 'items') + + +# * Code review nav * +# ============================================================================ +# ← Prev: _orderedbase.py Current: _orderedbidict.py +# ============================================================================ diff --git a/tapdown/lib/python3.11/site-packages/bidict/_typing.py b/tapdown/lib/python3.11/site-packages/bidict/_typing.py new file mode 100644 index 0000000..ce95053 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/_typing.py @@ -0,0 +1,49 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + + +"""Provide typing-related objects.""" + +from __future__ import annotations + +import typing as t +from enum import Enum + + +KT = t.TypeVar('KT') +VT = t.TypeVar('VT') +VT_co = t.TypeVar('VT_co', covariant=True) + + +Items = t.Iterable[t.Tuple[KT, VT]] + + +@t.runtime_checkable +class Maplike(t.Protocol[KT, VT_co]): + """Like typeshed's SupportsKeysAndGetItem, but usable at runtime.""" + + def keys(self) -> t.Iterable[KT]: ... + + def __getitem__(self, __key: KT) -> VT_co: ... + + +MapOrItems = t.Union[Maplike[KT, VT], Items[KT, VT]] +MappOrItems = t.Union[t.Mapping[KT, VT], Items[KT, VT]] +ItemsIter = t.Iterator[t.Tuple[KT, VT]] + + +class MissingT(Enum): + """Sentinel used to represent none/missing when None itself can't be used.""" + + MISSING = 'MISSING' + + +MISSING: t.Final[t.Literal[MissingT.MISSING]] = MissingT.MISSING +OKT = t.Union[KT, MissingT] #: optional key type +OVT = t.Union[VT, MissingT] #: optional value type + +DT = t.TypeVar('DT') #: for default arguments +ODT = t.Union[DT, MissingT] #: optional default arg type diff --git a/tapdown/lib/python3.11/site-packages/bidict/metadata.py b/tapdown/lib/python3.11/site-packages/bidict/metadata.py new file mode 100644 index 0000000..30ad836 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/metadata.py @@ -0,0 +1,14 @@ +# Copyright 2009-2024 Joshua Bronson. All rights reserved. +# +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +"""Define bidict package metadata.""" + +__version__ = '0.23.1' +__author__ = {'name': 'Joshua Bronson', 'email': 'jabronson@gmail.com'} +__copyright__ = '© 2009-2024 Joshua Bronson' +__description__ = 'The bidirectional mapping library for Python.' +__license__ = 'MPL 2.0' +__url__ = 'https://bidict.readthedocs.io' diff --git a/tapdown/lib/python3.11/site-packages/bidict/py.typed b/tapdown/lib/python3.11/site-packages/bidict/py.typed new file mode 100644 index 0000000..342ea76 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/bidict/py.typed @@ -0,0 +1 @@ +PEP-561 marker. diff --git a/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/LICENSE.txt b/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/LICENSE.txt new file mode 100644 index 0000000..79c9825 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/METADATA new file mode 100644 index 0000000..6d343f5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/RECORD new file mode 100644 index 0000000..52d1667 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/blinker-1.9.0.dist-info/WHEEL new file mode 100644 index 0000000..e3c6fee --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/blinker/__init__.py b/tapdown/lib/python3.11/site-packages/blinker/__init__.py new file mode 100644 index 0000000..1772fa4 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/blinker/_utilities.py b/tapdown/lib/python3.11/site-packages/blinker/_utilities.py new file mode 100644 index 0000000..000c902 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/blinker/base.py b/tapdown/lib/python3.11/site-packages/blinker/base.py new file mode 100644 index 0000000..d051b94 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/blinker/py.typed b/tapdown/lib/python3.11/site-packages/blinker/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/METADATA new file mode 100644 index 0000000..534eb57 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/RECORD new file mode 100644 index 0000000..9a1cb36 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/licenses/LICENSE.txt b/tapdown/lib/python3.11/site-packages/click-8.3.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..d12a849 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/__init__.py b/tapdown/lib/python3.11/site-packages/click/__init__.py new file mode 100644 index 0000000..1aa547c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/_compat.py b/tapdown/lib/python3.11/site-packages/click/_compat.py new file mode 100644 index 0000000..f2726b9 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/_termui_impl.py b/tapdown/lib/python3.11/site-packages/click/_termui_impl.py new file mode 100644 index 0000000..47f87b8 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/_textwrap.py b/tapdown/lib/python3.11/site-packages/click/_textwrap.py new file mode 100644 index 0000000..97fbee3 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/_utils.py b/tapdown/lib/python3.11/site-packages/click/_utils.py new file mode 100644 index 0000000..09fb008 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/_winconsole.py b/tapdown/lib/python3.11/site-packages/click/_winconsole.py new file mode 100644 index 0000000..e56c7c6 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/core.py b/tapdown/lib/python3.11/site-packages/click/core.py new file mode 100644 index 0000000..ff2f74a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/decorators.py b/tapdown/lib/python3.11/site-packages/click/decorators.py new file mode 100644 index 0000000..21f4c34 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/exceptions.py b/tapdown/lib/python3.11/site-packages/click/exceptions.py new file mode 100644 index 0000000..4d782ee --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/formatting.py b/tapdown/lib/python3.11/site-packages/click/formatting.py new file mode 100644 index 0000000..0b64f83 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/globals.py b/tapdown/lib/python3.11/site-packages/click/globals.py new file mode 100644 index 0000000..a2f9172 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/parser.py b/tapdown/lib/python3.11/site-packages/click/parser.py new file mode 100644 index 0000000..1ea1f71 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/py.typed b/tapdown/lib/python3.11/site-packages/click/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/click/shell_completion.py b/tapdown/lib/python3.11/site-packages/click/shell_completion.py new file mode 100644 index 0000000..8f1564c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/termui.py b/tapdown/lib/python3.11/site-packages/click/termui.py new file mode 100644 index 0000000..dcbb222 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/testing.py b/tapdown/lib/python3.11/site-packages/click/testing.py new file mode 100644 index 0000000..f6f60b8 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/types.py b/tapdown/lib/python3.11/site-packages/click/types.py new file mode 100644 index 0000000..e71c1c2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/click/utils.py b/tapdown/lib/python3.11/site-packages/click/utils.py new file mode 100644 index 0000000..beae26f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dateutil/__init__.py b/tapdown/lib/python3.11/site-packages/dateutil/__init__.py new file mode 100644 index 0000000..a2c19c0 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/__init__.py @@ -0,0 +1,24 @@ +# -*- coding: utf-8 -*- +import sys + +try: + from ._version import version as __version__ +except ImportError: + __version__ = 'unknown' + +__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz', + 'utils', 'zoneinfo'] + +def __getattr__(name): + import importlib + + if name in __all__: + return importlib.import_module("." + name, __name__) + raise AttributeError( + "module {!r} has not attribute {!r}".format(__name__, name) + ) + + +def __dir__(): + # __dir__ should include all the lazy-importable modules as well. + return [x for x in globals() if x not in sys.modules] + __all__ diff --git a/tapdown/lib/python3.11/site-packages/dateutil/_common.py b/tapdown/lib/python3.11/site-packages/dateutil/_common.py new file mode 100644 index 0000000..4eb2659 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/_common.py @@ -0,0 +1,43 @@ +""" +Common code used in multiple modules. +""" + + +class weekday(object): + __slots__ = ["weekday", "n"] + + def __init__(self, weekday, n=None): + self.weekday = weekday + self.n = n + + def __call__(self, n): + if n == self.n: + return self + else: + return self.__class__(self.weekday, n) + + def __eq__(self, other): + try: + if self.weekday != other.weekday or self.n != other.n: + return False + except AttributeError: + return False + return True + + def __hash__(self): + return hash(( + self.weekday, + self.n, + )) + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] + if not self.n: + return s + else: + return "%s(%+d)" % (s, self.n) + +# vim:ts=4:sw=4:et diff --git a/tapdown/lib/python3.11/site-packages/dateutil/_version.py b/tapdown/lib/python3.11/site-packages/dateutil/_version.py new file mode 100644 index 0000000..ddda980 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/_version.py @@ -0,0 +1,4 @@ +# file generated by setuptools_scm +# don't change, don't track in version control +__version__ = version = '2.9.0.post0' +__version_tuple__ = version_tuple = (2, 9, 0) diff --git a/tapdown/lib/python3.11/site-packages/dateutil/easter.py b/tapdown/lib/python3.11/site-packages/dateutil/easter.py new file mode 100644 index 0000000..f74d1f7 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/easter.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic Easter computing method for any given year, using +Western, Orthodox or Julian algorithms. +""" + +import datetime + +__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"] + +EASTER_JULIAN = 1 +EASTER_ORTHODOX = 2 +EASTER_WESTERN = 3 + + +def easter(year, method=EASTER_WESTERN): + """ + This method was ported from the work done by GM Arts, + on top of the algorithm by Claus Tondering, which was + based in part on the algorithm of Ouding (1940), as + quoted in "Explanatory Supplement to the Astronomical + Almanac", P. Kenneth Seidelmann, editor. + + This algorithm implements three different Easter + calculation methods: + + 1. Original calculation in Julian calendar, valid in + dates after 326 AD + 2. Original method, with date converted to Gregorian + calendar, valid in years 1583 to 4099 + 3. Revised method, in Gregorian calendar, valid in + years 1583 to 4099 as well + + These methods are represented by the constants: + + * ``EASTER_JULIAN = 1`` + * ``EASTER_ORTHODOX = 2`` + * ``EASTER_WESTERN = 3`` + + The default method is method 3. + + More about the algorithm may be found at: + + `GM Arts: Easter Algorithms `_ + + and + + `The Calendar FAQ: Easter `_ + + """ + + if not (1 <= method <= 3): + raise ValueError("invalid method") + + # g - Golden year - 1 + # c - Century + # h - (23 - Epact) mod 30 + # i - Number of days from March 21 to Paschal Full Moon + # j - Weekday for PFM (0=Sunday, etc) + # p - Number of days from March 21 to Sunday on or before PFM + # (-6 to 28 methods 1 & 3, to 56 for method 2) + # e - Extra days to add for method 2 (converting Julian + # date to Gregorian date) + + y = year + g = y % 19 + e = 0 + if method < 3: + # Old method + i = (19*g + 15) % 30 + j = (y + y//4 + i) % 7 + if method == 2: + # Extra dates to convert Julian to Gregorian date + e = 10 + if y > 1600: + e = e + y//100 - 16 - (y//100 - 16)//4 + else: + # New method + c = y//100 + h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30 + i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11)) + j = (y + y//4 + i + 2 - c + c//4) % 7 + + # p can be from -6 to 56 corresponding to dates 22 March to 23 May + # (later dates apply to method 2, although 23 May never actually occurs) + p = i - j + e + d = 1 + (p + 27 + (p + 6)//40) % 31 + m = 3 + (p + 26)//30 + return datetime.date(int(y), int(m), int(d)) diff --git a/tapdown/lib/python3.11/site-packages/dateutil/parser/__init__.py b/tapdown/lib/python3.11/site-packages/dateutil/parser/__init__.py new file mode 100644 index 0000000..d174b0e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/parser/__init__.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- +from ._parser import parse, parser, parserinfo, ParserError +from ._parser import DEFAULTPARSER, DEFAULTTZPARSER +from ._parser import UnknownTimezoneWarning + +from ._parser import __doc__ + +from .isoparser import isoparser, isoparse + +__all__ = ['parse', 'parser', 'parserinfo', + 'isoparse', 'isoparser', + 'ParserError', + 'UnknownTimezoneWarning'] + + +### +# Deprecate portions of the private interface so that downstream code that +# is improperly relying on it is given *some* notice. + + +def __deprecated_private_func(f): + from functools import wraps + import warnings + + msg = ('{name} is a private function and may break without warning, ' + 'it will be moved and or renamed in future versions.') + msg = msg.format(name=f.__name__) + + @wraps(f) + def deprecated_func(*args, **kwargs): + warnings.warn(msg, DeprecationWarning) + return f(*args, **kwargs) + + return deprecated_func + +def __deprecate_private_class(c): + import warnings + + msg = ('{name} is a private class and may break without warning, ' + 'it will be moved and or renamed in future versions.') + msg = msg.format(name=c.__name__) + + class private_class(c): + __doc__ = c.__doc__ + + def __init__(self, *args, **kwargs): + warnings.warn(msg, DeprecationWarning) + super(private_class, self).__init__(*args, **kwargs) + + private_class.__name__ = c.__name__ + + return private_class + + +from ._parser import _timelex, _resultbase +from ._parser import _tzparser, _parsetz + +_timelex = __deprecate_private_class(_timelex) +_tzparser = __deprecate_private_class(_tzparser) +_resultbase = __deprecate_private_class(_resultbase) +_parsetz = __deprecated_private_func(_parsetz) diff --git a/tapdown/lib/python3.11/site-packages/dateutil/parser/_parser.py b/tapdown/lib/python3.11/site-packages/dateutil/parser/_parser.py new file mode 100644 index 0000000..37d1663 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/parser/_parser.py @@ -0,0 +1,1613 @@ +# -*- coding: utf-8 -*- +""" +This module offers a generic date/time string parser which is able to parse +most known formats to represent a date and/or time. + +This module attempts to be forgiving with regards to unlikely input formats, +returning a datetime object even for dates which are ambiguous. If an element +of a date/time stamp is omitted, the following rules are applied: + +- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour + on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is + specified. +- If a time zone is omitted, a timezone-naive datetime is returned. + +If any other elements are missing, they are taken from the +:class:`datetime.datetime` object passed to the parameter ``default``. If this +results in a day number exceeding the valid number of days per month, the +value falls back to the end of the month. + +Additional resources about date/time string formats can be found below: + +- `A summary of the international standard date and time notation + `_ +- `W3C Date and Time Formats `_ +- `Time Formats (Planetary Rings Node) `_ +- `CPAN ParseDate module + `_ +- `Java SimpleDateFormat Class + `_ +""" +from __future__ import unicode_literals + +import datetime +import re +import string +import time +import warnings + +from calendar import monthrange +from io import StringIO + +import six +from six import integer_types, text_type + +from decimal import Decimal + +from warnings import warn + +from .. import relativedelta +from .. import tz + +__all__ = ["parse", "parserinfo", "ParserError"] + + +# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth +# making public and/or figuring out if there is something we can +# take off their plate. +class _timelex(object): + # Fractional seconds are sometimes split by a comma + _split_decimal = re.compile("([.,])") + + def __init__(self, instream): + if isinstance(instream, (bytes, bytearray)): + instream = instream.decode() + + if isinstance(instream, text_type): + instream = StringIO(instream) + elif getattr(instream, 'read', None) is None: + raise TypeError('Parser must be a string or character stream, not ' + '{itype}'.format(itype=instream.__class__.__name__)) + + self.instream = instream + self.charstack = [] + self.tokenstack = [] + self.eof = False + + def get_token(self): + """ + This function breaks the time string into lexical units (tokens), which + can be parsed by the parser. Lexical units are demarcated by changes in + the character set, so any continuous string of letters is considered + one unit, any continuous string of numbers is considered one unit. + + The main complication arises from the fact that dots ('.') can be used + both as separators (e.g. "Sep.20.2009") or decimal points (e.g. + "4:30:21.447"). As such, it is necessary to read the full context of + any dot-separated strings before breaking it into tokens; as such, this + function maintains a "token stack", for when the ambiguous context + demands that multiple tokens be parsed at once. + """ + if self.tokenstack: + return self.tokenstack.pop(0) + + seenletters = False + token = None + state = None + + while not self.eof: + # We only realize that we've reached the end of a token when we + # find a character that's not part of the current token - since + # that character may be part of the next token, it's stored in the + # charstack. + if self.charstack: + nextchar = self.charstack.pop(0) + else: + nextchar = self.instream.read(1) + while nextchar == '\x00': + nextchar = self.instream.read(1) + + if not nextchar: + self.eof = True + break + elif not state: + # First character of the token - determines if we're starting + # to parse a word, a number or something else. + token = nextchar + if self.isword(nextchar): + state = 'a' + elif self.isnum(nextchar): + state = '0' + elif self.isspace(nextchar): + token = ' ' + break # emit token + else: + break # emit token + elif state == 'a': + # If we've already started reading a word, we keep reading + # letters until we find something that's not part of a word. + seenletters = True + if self.isword(nextchar): + token += nextchar + elif nextchar == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0': + # If we've already started reading a number, we keep reading + # numbers until we find something that doesn't fit. + if self.isnum(nextchar): + token += nextchar + elif nextchar == '.' or (nextchar == ',' and len(token) >= 2): + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == 'a.': + # If we've seen some letters and a dot separator, continue + # parsing, and the tokens will be broken up later. + seenletters = True + if nextchar == '.' or self.isword(nextchar): + token += nextchar + elif self.isnum(nextchar) and token[-1] == '.': + token += nextchar + state = '0.' + else: + self.charstack.append(nextchar) + break # emit token + elif state == '0.': + # If we've seen at least one dot separator, keep going, we'll + # break up the tokens later. + if nextchar == '.' or self.isnum(nextchar): + token += nextchar + elif self.isword(nextchar) and token[-1] == '.': + token += nextchar + state = 'a.' + else: + self.charstack.append(nextchar) + break # emit token + + if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or + token[-1] in '.,')): + l = self._split_decimal.split(token) + token = l[0] + for tok in l[1:]: + if tok: + self.tokenstack.append(tok) + + if state == '0.' and token.count('.') == 0: + token = token.replace(',', '.') + + return token + + def __iter__(self): + return self + + def __next__(self): + token = self.get_token() + if token is None: + raise StopIteration + + return token + + def next(self): + return self.__next__() # Python 2.x support + + @classmethod + def split(cls, s): + return list(cls(s)) + + @classmethod + def isword(cls, nextchar): + """ Whether or not the next character is part of a word """ + return nextchar.isalpha() + + @classmethod + def isnum(cls, nextchar): + """ Whether the next character is part of a number """ + return nextchar.isdigit() + + @classmethod + def isspace(cls, nextchar): + """ Whether the next character is whitespace """ + return nextchar.isspace() + + +class _resultbase(object): + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def _repr(self, classname): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (classname, ", ".join(l)) + + def __len__(self): + return (sum(getattr(self, attr) is not None + for attr in self.__slots__)) + + def __repr__(self): + return self._repr(self.__class__.__name__) + + +class parserinfo(object): + """ + Class which handles what inputs are accepted. Subclass this to customize + the language and acceptable values for each parameter. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. Default is ``False``. + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + Default is ``False``. + """ + + # m from a.m/p.m, t from ISO T separator + JUMP = [" ", ".", ",", ";", "-", "/", "'", + "at", "on", "and", "ad", "m", "t", "of", + "st", "nd", "rd", "th"] + + WEEKDAYS = [("Mon", "Monday"), + ("Tue", "Tuesday"), # TODO: "Tues" + ("Wed", "Wednesday"), + ("Thu", "Thursday"), # TODO: "Thurs" + ("Fri", "Friday"), + ("Sat", "Saturday"), + ("Sun", "Sunday")] + MONTHS = [("Jan", "January"), + ("Feb", "February"), # TODO: "Febr" + ("Mar", "March"), + ("Apr", "April"), + ("May", "May"), + ("Jun", "June"), + ("Jul", "July"), + ("Aug", "August"), + ("Sep", "Sept", "September"), + ("Oct", "October"), + ("Nov", "November"), + ("Dec", "December")] + HMS = [("h", "hour", "hours"), + ("m", "minute", "minutes"), + ("s", "second", "seconds")] + AMPM = [("am", "a"), + ("pm", "p")] + UTCZONE = ["UTC", "GMT", "Z", "z"] + PERTAIN = ["of"] + TZOFFSET = {} + # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate", + # "Anno Domini", "Year of Our Lord"] + + def __init__(self, dayfirst=False, yearfirst=False): + self._jump = self._convert(self.JUMP) + self._weekdays = self._convert(self.WEEKDAYS) + self._months = self._convert(self.MONTHS) + self._hms = self._convert(self.HMS) + self._ampm = self._convert(self.AMPM) + self._utczone = self._convert(self.UTCZONE) + self._pertain = self._convert(self.PERTAIN) + + self.dayfirst = dayfirst + self.yearfirst = yearfirst + + self._year = time.localtime().tm_year + self._century = self._year // 100 * 100 + + def _convert(self, lst): + dct = {} + for i, v in enumerate(lst): + if isinstance(v, tuple): + for v in v: + dct[v.lower()] = i + else: + dct[v.lower()] = i + return dct + + def jump(self, name): + return name.lower() in self._jump + + def weekday(self, name): + try: + return self._weekdays[name.lower()] + except KeyError: + pass + return None + + def month(self, name): + try: + return self._months[name.lower()] + 1 + except KeyError: + pass + return None + + def hms(self, name): + try: + return self._hms[name.lower()] + except KeyError: + return None + + def ampm(self, name): + try: + return self._ampm[name.lower()] + except KeyError: + return None + + def pertain(self, name): + return name.lower() in self._pertain + + def utczone(self, name): + return name.lower() in self._utczone + + def tzoffset(self, name): + if name in self._utczone: + return 0 + + return self.TZOFFSET.get(name) + + def convertyear(self, year, century_specified=False): + """ + Converts two-digit years to year within [-50, 49] + range of self._year (current local time) + """ + + # Function contract is that the year is always positive + assert year >= 0 + + if year < 100 and not century_specified: + # assume current century to start + year += self._century + + if year >= self._year + 50: # if too far in future + year -= 100 + elif year < self._year - 50: # if too far in past + year += 100 + + return year + + def validate(self, res): + # move to info + if res.year is not None: + res.year = self.convertyear(res.year, res.century_specified) + + if ((res.tzoffset == 0 and not res.tzname) or + (res.tzname == 'Z' or res.tzname == 'z')): + res.tzname = "UTC" + res.tzoffset = 0 + elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): + res.tzoffset = 0 + return True + + +class _ymd(list): + def __init__(self, *args, **kwargs): + super(self.__class__, self).__init__(*args, **kwargs) + self.century_specified = False + self.dstridx = None + self.mstridx = None + self.ystridx = None + + @property + def has_year(self): + return self.ystridx is not None + + @property + def has_month(self): + return self.mstridx is not None + + @property + def has_day(self): + return self.dstridx is not None + + def could_be_day(self, value): + if self.has_day: + return False + elif not self.has_month: + return 1 <= value <= 31 + elif not self.has_year: + # Be permissive, assume leap year + month = self[self.mstridx] + return 1 <= value <= monthrange(2000, month)[1] + else: + month = self[self.mstridx] + year = self[self.ystridx] + return 1 <= value <= monthrange(year, month)[1] + + def append(self, val, label=None): + if hasattr(val, '__len__'): + if val.isdigit() and len(val) > 2: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + elif val > 100: + self.century_specified = True + if label not in [None, 'Y']: # pragma: no cover + raise ValueError(label) + label = 'Y' + + super(self.__class__, self).append(int(val)) + + if label == 'M': + if self.has_month: + raise ValueError('Month is already set') + self.mstridx = len(self) - 1 + elif label == 'D': + if self.has_day: + raise ValueError('Day is already set') + self.dstridx = len(self) - 1 + elif label == 'Y': + if self.has_year: + raise ValueError('Year is already set') + self.ystridx = len(self) - 1 + + def _resolve_from_stridxs(self, strids): + """ + Try to resolve the identities of year/month/day elements using + ystridx, mstridx, and dstridx, if enough of these are specified. + """ + if len(self) == 3 and len(strids) == 2: + # we can back out the remaining stridx value + missing = [x for x in range(3) if x not in strids.values()] + key = [x for x in ['y', 'm', 'd'] if x not in strids] + assert len(missing) == len(key) == 1 + key = key[0] + val = missing[0] + strids[key] = val + + assert len(self) == len(strids) # otherwise this should not be called + out = {key: self[strids[key]] for key in strids} + return (out.get('y'), out.get('m'), out.get('d')) + + def resolve_ymd(self, yearfirst, dayfirst): + len_ymd = len(self) + year, month, day = (None, None, None) + + strids = (('y', self.ystridx), + ('m', self.mstridx), + ('d', self.dstridx)) + + strids = {key: val for key, val in strids if val is not None} + if (len(self) == len(strids) > 0 or + (len(self) == 3 and len(strids) == 2)): + return self._resolve_from_stridxs(strids) + + mstridx = self.mstridx + + if len_ymd > 3: + raise ValueError("More than three YMD values") + elif len_ymd == 1 or (mstridx is not None and len_ymd == 2): + # One member, or two members with a month string + if mstridx is not None: + month = self[mstridx] + # since mstridx is 0 or 1, self[mstridx-1] always + # looks up the other element + other = self[mstridx - 1] + else: + other = self[0] + + if len_ymd > 1 or mstridx is None: + if other > 31: + year = other + else: + day = other + + elif len_ymd == 2: + # Two members with numbers + if self[0] > 31: + # 99-01 + year, month = self + elif self[1] > 31: + # 01-99 + month, year = self + elif dayfirst and self[1] <= 12: + # 13-01 + day, month = self + else: + # 01-13 + month, day = self + + elif len_ymd == 3: + # Three members + if mstridx == 0: + if self[1] > 31: + # Apr-2003-25 + month, year, day = self + else: + month, day, year = self + elif mstridx == 1: + if self[0] > 31 or (yearfirst and self[2] <= 31): + # 99-Jan-01 + year, month, day = self + else: + # 01-Jan-01 + # Give precedence to day-first, since + # two-digit years is usually hand-written. + day, month, year = self + + elif mstridx == 2: + # WTF!? + if self[1] > 31: + # 01-99-Jan + day, year, month = self + else: + # 99-01-Jan + year, day, month = self + + else: + if (self[0] > 31 or + self.ystridx == 0 or + (yearfirst and self[1] <= 12 and self[2] <= 31)): + # 99-01-01 + if dayfirst and self[2] <= 12: + year, day, month = self + else: + year, month, day = self + elif self[0] > 12 or (dayfirst and self[1] <= 12): + # 13-01-01 + day, month, year = self + else: + # 01-13-01 + month, day, year = self + + return year, month, day + + +class parser(object): + def __init__(self, info=None): + self.info = info or parserinfo() + + def parse(self, timestr, default=None, + ignoretz=False, tzinfos=None, **kwargs): + """ + Parse the date/time string into a :class:`datetime.datetime` object. + + :param timestr: + Any date/time string using the supported formats. + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a + naive :class:`datetime.datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param \\*\\*kwargs: + Keyword arguments as passed to ``_parse()``. + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ParserError: + Raised for invalid or unknown string format, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date + would be created. + + :raises TypeError: + Raised for non-string or character stream input. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + + if default is None: + default = datetime.datetime.now().replace(hour=0, minute=0, + second=0, microsecond=0) + + res, skipped_tokens = self._parse(timestr, **kwargs) + + if res is None: + raise ParserError("Unknown string format: %s", timestr) + + if len(res) == 0: + raise ParserError("String does not contain a date: %s", timestr) + + try: + ret = self._build_naive(res, default) + except ValueError as e: + six.raise_from(ParserError(str(e) + ": %s", timestr), e) + + if not ignoretz: + ret = self._build_tzaware(ret, res, tzinfos) + + if kwargs.get('fuzzy_with_tokens', False): + return ret, skipped_tokens + else: + return ret + + class _result(_resultbase): + __slots__ = ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond", + "tzname", "tzoffset", "ampm","any_unused_tokens"] + + def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, + fuzzy_with_tokens=False): + """ + Private method which performs the heavy lifting of parsing, called from + ``parse()``, which passes on its ``kwargs`` to this function. + + :param timestr: + The string to parse. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM + and YMD. If set to ``None``, this value is retrieved from the + current :class:`parserinfo` object (which itself defaults to + ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken + to be the year, otherwise the last number is taken to be the year. + If this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + """ + if fuzzy_with_tokens: + fuzzy = True + + info = self.info + + if dayfirst is None: + dayfirst = info.dayfirst + + if yearfirst is None: + yearfirst = info.yearfirst + + res = self._result() + l = _timelex.split(timestr) # Splits the timestr into tokens + + skipped_idxs = [] + + # year/month/day list + ymd = _ymd() + + len_l = len(l) + i = 0 + try: + while i < len_l: + + # Check if it's a number + value_repr = l[i] + try: + value = float(value_repr) + except ValueError: + value = None + + if value is not None: + # Numeric token + i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy) + + # Check weekday + elif info.weekday(l[i]) is not None: + value = info.weekday(l[i]) + res.weekday = value + + # Check month name + elif info.month(l[i]) is not None: + value = info.month(l[i]) + ymd.append(value, 'M') + + if i + 1 < len_l: + if l[i + 1] in ('-', '/'): + # Jan-01[-99] + sep = l[i + 1] + ymd.append(l[i + 2]) + + if i + 3 < len_l and l[i + 3] == sep: + # Jan-01-99 + ymd.append(l[i + 4]) + i += 2 + + i += 2 + + elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and + info.pertain(l[i + 2])): + # Jan of 01 + # In this case, 01 is clearly year + if l[i + 4].isdigit(): + # Convert it here to become unambiguous + value = int(l[i + 4]) + year = str(info.convertyear(value)) + ymd.append(year, 'Y') + else: + # Wrong guess + pass + # TODO: not hit in tests + i += 4 + + # Check am/pm + elif info.ampm(l[i]) is not None: + value = info.ampm(l[i]) + val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy) + + if val_is_ampm: + res.hour = self._adjust_ampm(res.hour, value) + res.ampm = value + + elif fuzzy: + skipped_idxs.append(i) + + # Check for a timezone name + elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]): + res.tzname = l[i] + res.tzoffset = info.tzoffset(res.tzname) + + # Check for something like GMT+3, or BRST+3. Notice + # that it doesn't mean "I am 3 hours after GMT", but + # "my time +3 is GMT". If found, we reverse the + # logic so that timezone parsing code will get it + # right. + if i + 1 < len_l and l[i + 1] in ('+', '-'): + l[i + 1] = ('+', '-')[l[i + 1] == '+'] + res.tzoffset = None + if info.utczone(res.tzname): + # With something like GMT+3, the timezone + # is *not* GMT. + res.tzname = None + + # Check for a numbered timezone + elif res.hour is not None and l[i] in ('+', '-'): + signal = (-1, 1)[l[i] == '+'] + len_li = len(l[i + 1]) + + # TODO: check that l[i + 1] is integer? + if len_li == 4: + # -0300 + hour_offset = int(l[i + 1][:2]) + min_offset = int(l[i + 1][2:]) + elif i + 2 < len_l and l[i + 2] == ':': + # -03:00 + hour_offset = int(l[i + 1]) + min_offset = int(l[i + 3]) # TODO: Check that l[i+3] is minute-like? + i += 2 + elif len_li <= 2: + # -[0]3 + hour_offset = int(l[i + 1][:2]) + min_offset = 0 + else: + raise ValueError(timestr) + + res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60) + + # Look for a timezone name between parenthesis + if (i + 5 < len_l and + info.jump(l[i + 2]) and l[i + 3] == '(' and + l[i + 5] == ')' and + 3 <= len(l[i + 4]) and + self._could_be_tzname(res.hour, res.tzname, + None, l[i + 4])): + # -0300 (BRST) + res.tzname = l[i + 4] + i += 4 + + i += 1 + + # Check jumps + elif not (info.jump(l[i]) or fuzzy): + raise ValueError(timestr) + + else: + skipped_idxs.append(i) + i += 1 + + # Process year/month/day + year, month, day = ymd.resolve_ymd(yearfirst, dayfirst) + + res.century_specified = ymd.century_specified + res.year = year + res.month = month + res.day = day + + except (IndexError, ValueError): + return None, None + + if not info.validate(res): + return None, None + + if fuzzy_with_tokens: + skipped_tokens = self._recombine_skipped(l, skipped_idxs) + return res, tuple(skipped_tokens) + else: + return res, None + + def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy): + # Token is a number + value_repr = tokens[idx] + try: + value = self._to_decimal(value_repr) + except Exception as e: + six.raise_from(ValueError('Unknown numeric token'), e) + + len_li = len(value_repr) + + len_l = len(tokens) + + if (len(ymd) == 3 and len_li in (2, 4) and + res.hour is None and + (idx + 1 >= len_l or + (tokens[idx + 1] != ':' and + info.hms(tokens[idx + 1]) is None))): + # 19990101T23[59] + s = tokens[idx] + res.hour = int(s[:2]) + + if len_li == 4: + res.minute = int(s[2:]) + + elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6): + # YYMMDD or HHMMSS[.ss] + s = tokens[idx] + + if not ymd and '.' not in tokens[idx]: + ymd.append(s[:2]) + ymd.append(s[2:4]) + ymd.append(s[4:]) + else: + # 19990101T235959[.59] + + # TODO: Check if res attributes already set. + res.hour = int(s[:2]) + res.minute = int(s[2:4]) + res.second, res.microsecond = self._parsems(s[4:]) + + elif len_li in (8, 12, 14): + # YYYYMMDD + s = tokens[idx] + ymd.append(s[:4], 'Y') + ymd.append(s[4:6]) + ymd.append(s[6:8]) + + if len_li > 8: + res.hour = int(s[8:10]) + res.minute = int(s[10:12]) + + if len_li > 12: + res.second = int(s[12:]) + + elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None: + # HH[ ]h or MM[ ]m or SS[.ss][ ]s + hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True) + (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx) + if hms is not None: + # TODO: checking that hour/minute/second are not + # already set? + self._assign_hms(res, value_repr, hms) + + elif idx + 2 < len_l and tokens[idx + 1] == ':': + # HH:MM[:SS[.ss]] + res.hour = int(value) + value = self._to_decimal(tokens[idx + 2]) # TODO: try/except for this? + (res.minute, res.second) = self._parse_min_sec(value) + + if idx + 4 < len_l and tokens[idx + 3] == ':': + res.second, res.microsecond = self._parsems(tokens[idx + 4]) + + idx += 2 + + idx += 2 + + elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'): + sep = tokens[idx + 1] + ymd.append(value_repr) + + if idx + 2 < len_l and not info.jump(tokens[idx + 2]): + if tokens[idx + 2].isdigit(): + # 01-01[-01] + ymd.append(tokens[idx + 2]) + else: + # 01-Jan[-01] + value = info.month(tokens[idx + 2]) + + if value is not None: + ymd.append(value, 'M') + else: + raise ValueError() + + if idx + 3 < len_l and tokens[idx + 3] == sep: + # We have three members + value = info.month(tokens[idx + 4]) + + if value is not None: + ymd.append(value, 'M') + else: + ymd.append(tokens[idx + 4]) + idx += 2 + + idx += 1 + idx += 1 + + elif idx + 1 >= len_l or info.jump(tokens[idx + 1]): + if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None: + # 12 am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2])) + idx += 1 + else: + # Year, month or day + ymd.append(value) + idx += 1 + + elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24): + # 12am + hour = int(value) + res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1])) + idx += 1 + + elif ymd.could_be_day(value): + ymd.append(value) + + elif not fuzzy: + raise ValueError() + + return idx + + def _find_hms_idx(self, idx, tokens, info, allow_jump): + len_l = len(tokens) + + if idx+1 < len_l and info.hms(tokens[idx+1]) is not None: + # There is an "h", "m", or "s" label following this token. We take + # assign the upcoming label to the current token. + # e.g. the "12" in 12h" + hms_idx = idx + 1 + + elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and + info.hms(tokens[idx+2]) is not None): + # There is a space and then an "h", "m", or "s" label. + # e.g. the "12" in "12 h" + hms_idx = idx + 2 + + elif idx > 0 and info.hms(tokens[idx-1]) is not None: + # There is a "h", "m", or "s" preceding this token. Since neither + # of the previous cases was hit, there is no label following this + # token, so we use the previous label. + # e.g. the "04" in "12h04" + hms_idx = idx-1 + + elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and + info.hms(tokens[idx-2]) is not None): + # If we are looking at the final token, we allow for a + # backward-looking check to skip over a space. + # TODO: Are we sure this is the right condition here? + hms_idx = idx - 2 + + else: + hms_idx = None + + return hms_idx + + def _assign_hms(self, res, value_repr, hms): + # See GH issue #427, fixing float rounding + value = self._to_decimal(value_repr) + + if hms == 0: + # Hour + res.hour = int(value) + if value % 1: + res.minute = int(60*(value % 1)) + + elif hms == 1: + (res.minute, res.second) = self._parse_min_sec(value) + + elif hms == 2: + (res.second, res.microsecond) = self._parsems(value_repr) + + def _could_be_tzname(self, hour, tzname, tzoffset, token): + return (hour is not None and + tzname is None and + tzoffset is None and + len(token) <= 5 and + (all(x in string.ascii_uppercase for x in token) + or token in self.info.UTCZONE)) + + def _ampm_valid(self, hour, ampm, fuzzy): + """ + For fuzzy parsing, 'a' or 'am' (both valid English words) + may erroneously trigger the AM/PM flag. Deal with that + here. + """ + val_is_ampm = True + + # If there's already an AM/PM flag, this one isn't one. + if fuzzy and ampm is not None: + val_is_ampm = False + + # If AM/PM is found and hour is not, raise a ValueError + if hour is None: + if fuzzy: + val_is_ampm = False + else: + raise ValueError('No hour specified with AM or PM flag.') + elif not 0 <= hour <= 12: + # If AM/PM is found, it's a 12 hour clock, so raise + # an error for invalid range + if fuzzy: + val_is_ampm = False + else: + raise ValueError('Invalid hour specified for 12-hour clock.') + + return val_is_ampm + + def _adjust_ampm(self, hour, ampm): + if hour < 12 and ampm == 1: + hour += 12 + elif hour == 12 and ampm == 0: + hour = 0 + return hour + + def _parse_min_sec(self, value): + # TODO: Every usage of this function sets res.second to the return + # value. Are there any cases where second will be returned as None and + # we *don't* want to set res.second = None? + minute = int(value) + second = None + + sec_remainder = value % 1 + if sec_remainder: + second = int(60 * sec_remainder) + return (minute, second) + + def _parse_hms(self, idx, tokens, info, hms_idx): + # TODO: Is this going to admit a lot of false-positives for when we + # just happen to have digits and "h", "m" or "s" characters in non-date + # text? I guess hex hashes won't have that problem, but there's plenty + # of random junk out there. + if hms_idx is None: + hms = None + new_idx = idx + elif hms_idx > idx: + hms = info.hms(tokens[hms_idx]) + new_idx = hms_idx + else: + # Looking backwards, increment one. + hms = info.hms(tokens[hms_idx]) + 1 + new_idx = idx + + return (new_idx, hms) + + # ------------------------------------------------------------------ + # Handling for individual tokens. These are kept as methods instead + # of functions for the sake of customizability via subclassing. + + def _parsems(self, value): + """Parse a I[.F] seconds value into (seconds, microseconds).""" + if "." not in value: + return int(value), 0 + else: + i, f = value.split(".") + return int(i), int(f.ljust(6, "0")[:6]) + + def _to_decimal(self, val): + try: + decimal_value = Decimal(val) + # See GH 662, edge case, infinite value should not be converted + # via `_to_decimal` + if not decimal_value.is_finite(): + raise ValueError("Converted decimal value is infinite or NaN") + except Exception as e: + msg = "Could not convert %s to decimal" % val + six.raise_from(ValueError(msg), e) + else: + return decimal_value + + # ------------------------------------------------------------------ + # Post-Parsing construction of datetime output. These are kept as + # methods instead of functions for the sake of customizability via + # subclassing. + + def _build_tzinfo(self, tzinfos, tzname, tzoffset): + if callable(tzinfos): + tzdata = tzinfos(tzname, tzoffset) + else: + tzdata = tzinfos.get(tzname) + # handle case where tzinfo is paased an options that returns None + # eg tzinfos = {'BRST' : None} + if isinstance(tzdata, datetime.tzinfo) or tzdata is None: + tzinfo = tzdata + elif isinstance(tzdata, text_type): + tzinfo = tz.tzstr(tzdata) + elif isinstance(tzdata, integer_types): + tzinfo = tz.tzoffset(tzname, tzdata) + else: + raise TypeError("Offset must be tzinfo subclass, tz string, " + "or int offset.") + return tzinfo + + def _build_tzaware(self, naive, res, tzinfos): + if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)): + tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset) + aware = naive.replace(tzinfo=tzinfo) + aware = self._assign_tzname(aware, res.tzname) + + elif res.tzname and res.tzname in time.tzname: + aware = naive.replace(tzinfo=tz.tzlocal()) + + # Handle ambiguous local datetime + aware = self._assign_tzname(aware, res.tzname) + + # This is mostly relevant for winter GMT zones parsed in the UK + if (aware.tzname() != res.tzname and + res.tzname in self.info.UTCZONE): + aware = aware.replace(tzinfo=tz.UTC) + + elif res.tzoffset == 0: + aware = naive.replace(tzinfo=tz.UTC) + + elif res.tzoffset: + aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) + + elif not res.tzname and not res.tzoffset: + # i.e. no timezone information was found. + aware = naive + + elif res.tzname: + # tz-like string was parsed but we don't know what to do + # with it + warnings.warn("tzname {tzname} identified but not understood. " + "Pass `tzinfos` argument in order to correctly " + "return a timezone-aware datetime. In a future " + "version, this will raise an " + "exception.".format(tzname=res.tzname), + category=UnknownTimezoneWarning) + aware = naive + + return aware + + def _build_naive(self, res, default): + repl = {} + for attr in ("year", "month", "day", "hour", + "minute", "second", "microsecond"): + value = getattr(res, attr) + if value is not None: + repl[attr] = value + + if 'day' not in repl: + # If the default day exceeds the last day of the month, fall back + # to the end of the month. + cyear = default.year if res.year is None else res.year + cmonth = default.month if res.month is None else res.month + cday = default.day if res.day is None else res.day + + if cday > monthrange(cyear, cmonth)[1]: + repl['day'] = monthrange(cyear, cmonth)[1] + + naive = default.replace(**repl) + + if res.weekday is not None and not res.day: + naive = naive + relativedelta.relativedelta(weekday=res.weekday) + + return naive + + def _assign_tzname(self, dt, tzname): + if dt.tzname() != tzname: + new_dt = tz.enfold(dt, fold=1) + if new_dt.tzname() == tzname: + return new_dt + + return dt + + def _recombine_skipped(self, tokens, skipped_idxs): + """ + >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] + >>> skipped_idxs = [0, 1, 2, 5] + >>> _recombine_skipped(tokens, skipped_idxs) + ["foo bar", "baz"] + """ + skipped_tokens = [] + for i, idx in enumerate(sorted(skipped_idxs)): + if i > 0 and idx - 1 == skipped_idxs[i - 1]: + skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] + else: + skipped_tokens.append(tokens[idx]) + + return skipped_tokens + + +DEFAULTPARSER = parser() + + +def parse(timestr, parserinfo=None, **kwargs): + """ + + Parse a string in one of the supported formats, using the + ``parserinfo`` parameters. + + :param timestr: + A string containing a date/time stamp. + + :param parserinfo: + A :class:`parserinfo` object containing parameters for the parser. + If ``None``, the default arguments to the :class:`parserinfo` + constructor are used. + + The ``**kwargs`` parameter takes the following keyword arguments: + + :param default: + The default datetime object, if this is a datetime object and not + ``None``, elements specified in ``timestr`` replace elements in the + default object. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime` object is returned. + + :param tzinfos: + Additional time zone names / aliases which may be present in the + string. This argument maps time zone names (and optionally offsets + from those time zones) to time zones. This parameter can be a + dictionary with timezone aliases mapping time zone names to time + zones or a function taking two parameters (``tzname`` and + ``tzoffset``) and returning a time zone. + + The timezones to which the names are mapped can be an integer + offset from UTC in seconds or a :class:`tzinfo` object. + + .. doctest:: + :options: +NORMALIZE_WHITESPACE + + >>> from dateutil.parser import parse + >>> from dateutil.tz import gettz + >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} + >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) + >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) + datetime.datetime(2012, 1, 19, 17, 21, + tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) + + This parameter is ignored if ``ignoretz`` is set. + + :param dayfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the day (``True``) or month (``False``). If + ``yearfirst`` is set to ``True``, this distinguishes between YDM and + YMD. If set to ``None``, this value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param yearfirst: + Whether to interpret the first value in an ambiguous 3-integer date + (e.g. 01/05/09) as the year. If ``True``, the first number is taken to + be the year, otherwise the last number is taken to be the year. If + this is set to ``None``, the value is retrieved from the current + :class:`parserinfo` object (which itself defaults to ``False``). + + :param fuzzy: + Whether to allow fuzzy parsing, allowing for string like "Today is + January 1, 2047 at 8:21:00AM". + + :param fuzzy_with_tokens: + If ``True``, ``fuzzy`` is automatically set to True, and the parser + will return a tuple where the first element is the parsed + :class:`datetime.datetime` datetimestamp and the second element is + a tuple containing the portions of the string which were ignored: + + .. doctest:: + + >>> from dateutil.parser import parse + >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) + (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) + + :return: + Returns a :class:`datetime.datetime` object or, if the + ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the + first element being a :class:`datetime.datetime` object, the second + a tuple containing the fuzzy tokens. + + :raises ParserError: + Raised for invalid or unknown string formats, if the provided + :class:`tzinfo` is not in a valid format, or if an invalid date would + be created. + + :raises OverflowError: + Raised if the parsed date exceeds the largest valid C integer on + your system. + """ + if parserinfo: + return parser(parserinfo).parse(timestr, **kwargs) + else: + return DEFAULTPARSER.parse(timestr, **kwargs) + + +class _tzparser(object): + + class _result(_resultbase): + + __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", + "start", "end"] + + class _attr(_resultbase): + __slots__ = ["month", "week", "weekday", + "yday", "jyday", "day", "time"] + + def __repr__(self): + return self._repr("") + + def __init__(self): + _resultbase.__init__(self) + self.start = self._attr() + self.end = self._attr() + + def parse(self, tzstr): + res = self._result() + l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x] + used_idxs = list() + try: + + len_l = len(l) + + i = 0 + while i < len_l: + # BRST+3[BRDT[+2]] + j = i + while j < len_l and not [x for x in l[j] + if x in "0123456789:,-+"]: + j += 1 + if j != i: + if not res.stdabbr: + offattr = "stdoffset" + res.stdabbr = "".join(l[i:j]) + else: + offattr = "dstoffset" + res.dstabbr = "".join(l[i:j]) + + for ii in range(j): + used_idxs.append(ii) + i = j + if (i < len_l and (l[i] in ('+', '-') or l[i][0] in + "0123456789")): + if l[i] in ('+', '-'): + # Yes, that's right. See the TZ variable + # documentation. + signal = (1, -1)[l[i] == '+'] + used_idxs.append(i) + i += 1 + else: + signal = -1 + len_li = len(l[i]) + if len_li == 4: + # -0300 + setattr(res, offattr, (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) * signal) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + setattr(res, offattr, + (int(l[i]) * 3600 + + int(l[i + 2]) * 60) * signal) + used_idxs.append(i) + i += 2 + elif len_li <= 2: + # -[0]3 + setattr(res, offattr, + int(l[i][:2]) * 3600 * signal) + else: + return None + used_idxs.append(i) + i += 1 + if res.dstabbr: + break + else: + break + + + if i < len_l: + for j in range(i, len_l): + if l[j] == ';': + l[j] = ',' + + assert l[i] == ',' + + i += 1 + + if i >= len_l: + pass + elif (8 <= l.count(',') <= 9 and + not [y for x in l[i:] if x != ',' + for y in x if y not in "0123456789+-"]): + # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] + for x in (res.start, res.end): + x.month = int(l[i]) + used_idxs.append(i) + i += 2 + if l[i] == '-': + value = int(l[i + 1]) * -1 + used_idxs.append(i) + i += 1 + else: + value = int(l[i]) + used_idxs.append(i) + i += 2 + if value: + x.week = value + x.weekday = (int(l[i]) - 1) % 7 + else: + x.day = int(l[i]) + used_idxs.append(i) + i += 2 + x.time = int(l[i]) + used_idxs.append(i) + i += 2 + if i < len_l: + if l[i] in ('-', '+'): + signal = (-1, 1)[l[i] == "+"] + used_idxs.append(i) + i += 1 + else: + signal = 1 + used_idxs.append(i) + res.dstoffset = (res.stdoffset + int(l[i]) * signal) + + # This was a made-up format that is not in normal use + warn(('Parsed time zone "%s"' % tzstr) + + 'is in a non-standard dateutil-specific format, which ' + + 'is now deprecated; support for parsing this format ' + + 'will be removed in future versions. It is recommended ' + + 'that you switch to a standard format like the GNU ' + + 'TZ variable format.', tz.DeprecatedTzFormatWarning) + elif (l.count(',') == 2 and l[i:].count('/') <= 2 and + not [y for x in l[i:] if x not in (',', '/', 'J', 'M', + '.', '-', ':') + for y in x if y not in "0123456789"]): + for x in (res.start, res.end): + if l[i] == 'J': + # non-leap year day (1 based) + used_idxs.append(i) + i += 1 + x.jyday = int(l[i]) + elif l[i] == 'M': + # month[-.]week[-.]weekday + used_idxs.append(i) + i += 1 + x.month = int(l[i]) + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.week = int(l[i]) + if x.week == 5: + x.week = -1 + used_idxs.append(i) + i += 1 + assert l[i] in ('-', '.') + used_idxs.append(i) + i += 1 + x.weekday = (int(l[i]) - 1) % 7 + else: + # year day (zero based) + x.yday = int(l[i]) + 1 + + used_idxs.append(i) + i += 1 + + if i < len_l and l[i] == '/': + used_idxs.append(i) + i += 1 + # start time + len_li = len(l[i]) + if len_li == 4: + # -0300 + x.time = (int(l[i][:2]) * 3600 + + int(l[i][2:]) * 60) + elif i + 1 < len_l and l[i + 1] == ':': + # -03:00 + x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60 + used_idxs.append(i) + i += 2 + if i + 1 < len_l and l[i + 1] == ':': + used_idxs.append(i) + i += 2 + x.time += int(l[i]) + elif len_li <= 2: + # -[0]3 + x.time = (int(l[i][:2]) * 3600) + else: + return None + used_idxs.append(i) + i += 1 + + assert i == len_l or l[i] == ',' + + i += 1 + + assert i >= len_l + + except (IndexError, ValueError, AssertionError): + return None + + unused_idxs = set(range(len_l)).difference(used_idxs) + res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"}) + return res + + +DEFAULTTZPARSER = _tzparser() + + +def _parsetz(tzstr): + return DEFAULTTZPARSER.parse(tzstr) + + +class ParserError(ValueError): + """Exception subclass used for any failure to parse a datetime string. + + This is a subclass of :py:exc:`ValueError`, and should be raised any time + earlier versions of ``dateutil`` would have raised ``ValueError``. + + .. versionadded:: 2.8.1 + """ + def __str__(self): + try: + return self.args[0] % self.args[1:] + except (TypeError, IndexError): + return super(ParserError, self).__str__() + + def __repr__(self): + args = ", ".join("'%s'" % arg for arg in self.args) + return "%s(%s)" % (self.__class__.__name__, args) + + +class UnknownTimezoneWarning(RuntimeWarning): + """Raised when the parser finds a timezone it cannot parse into a tzinfo. + + .. versionadded:: 2.7.0 + """ +# vim:ts=4:sw=4:et diff --git a/tapdown/lib/python3.11/site-packages/dateutil/parser/isoparser.py b/tapdown/lib/python3.11/site-packages/dateutil/parser/isoparser.py new file mode 100644 index 0000000..7060087 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/parser/isoparser.py @@ -0,0 +1,416 @@ +# -*- coding: utf-8 -*- +""" +This module offers a parser for ISO-8601 strings + +It is intended to support all valid date, time and datetime formats per the +ISO-8601 specification. + +..versionadded:: 2.7.0 +""" +from datetime import datetime, timedelta, time, date +import calendar +from dateutil import tz + +from functools import wraps + +import re +import six + +__all__ = ["isoparse", "isoparser"] + + +def _takes_ascii(f): + @wraps(f) + def func(self, str_in, *args, **kwargs): + # If it's a stream, read the whole thing + str_in = getattr(str_in, 'read', lambda: str_in)() + + # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII + if isinstance(str_in, six.text_type): + # ASCII is the same in UTF-8 + try: + str_in = str_in.encode('ascii') + except UnicodeEncodeError as e: + msg = 'ISO-8601 strings should contain only ASCII characters' + six.raise_from(ValueError(msg), e) + + return f(self, str_in, *args, **kwargs) + + return func + + +class isoparser(object): + def __init__(self, sep=None): + """ + :param sep: + A single character that separates date and time portions. If + ``None``, the parser will accept any single character. + For strict ISO-8601 adherence, pass ``'T'``. + """ + if sep is not None: + if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'): + raise ValueError('Separator must be a single, non-numeric ' + + 'ASCII character') + + sep = sep.encode('ascii') + + self._sep = sep + + @_takes_ascii + def isoparse(self, dt_str): + """ + Parse an ISO-8601 datetime string into a :class:`datetime.datetime`. + + An ISO-8601 datetime string consists of a date portion, followed + optionally by a time portion - the date and time portions are separated + by a single character separator, which is ``T`` in the official + standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be + combined with a time portion. + + Supported date formats are: + + Common: + + - ``YYYY`` + - ``YYYY-MM`` + - ``YYYY-MM-DD`` or ``YYYYMMDD`` + + Uncommon: + + - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0) + - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day + + The ISO week and day numbering follows the same logic as + :func:`datetime.date.isocalendar`. + + Supported time formats are: + + - ``hh`` + - ``hh:mm`` or ``hhmm`` + - ``hh:mm:ss`` or ``hhmmss`` + - ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits) + + Midnight is a special case for `hh`, as the standard supports both + 00:00 and 24:00 as a representation. The decimal separator can be + either a dot or a comma. + + + .. caution:: + + Support for fractional components other than seconds is part of the + ISO-8601 standard, but is not currently implemented in this parser. + + Supported time zone offset formats are: + + - `Z` (UTC) + - `±HH:MM` + - `±HHMM` + - `±HH` + + Offsets will be represented as :class:`dateutil.tz.tzoffset` objects, + with the exception of UTC, which will be represented as + :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such + as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`. + + :param dt_str: + A string or stream containing only an ISO-8601 datetime string + + :return: + Returns a :class:`datetime.datetime` representing the string. + Unspecified components default to their lowest value. + + .. warning:: + + As of version 2.7.0, the strictness of the parser should not be + considered a stable part of the contract. Any valid ISO-8601 string + that parses correctly with the default settings will continue to + parse correctly in future versions, but invalid strings that + currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not + guaranteed to continue failing in future versions if they encode + a valid date. + + .. versionadded:: 2.7.0 + """ + components, pos = self._parse_isodate(dt_str) + + if len(dt_str) > pos: + if self._sep is None or dt_str[pos:pos + 1] == self._sep: + components += self._parse_isotime(dt_str[pos + 1:]) + else: + raise ValueError('String contains unknown ISO components') + + if len(components) > 3 and components[3] == 24: + components[3] = 0 + return datetime(*components) + timedelta(days=1) + + return datetime(*components) + + @_takes_ascii + def parse_isodate(self, datestr): + """ + Parse the date portion of an ISO string. + + :param datestr: + The string portion of an ISO string, without a separator + + :return: + Returns a :class:`datetime.date` object + """ + components, pos = self._parse_isodate(datestr) + if pos < len(datestr): + raise ValueError('String contains unknown ISO ' + + 'components: {!r}'.format(datestr.decode('ascii'))) + return date(*components) + + @_takes_ascii + def parse_isotime(self, timestr): + """ + Parse the time portion of an ISO string. + + :param timestr: + The time portion of an ISO string, without a separator + + :return: + Returns a :class:`datetime.time` object + """ + components = self._parse_isotime(timestr) + if components[0] == 24: + components[0] = 0 + return time(*components) + + @_takes_ascii + def parse_tzstr(self, tzstr, zero_as_utc=True): + """ + Parse a valid ISO time zone string. + + See :func:`isoparser.isoparse` for details on supported formats. + + :param tzstr: + A string representing an ISO time zone offset + + :param zero_as_utc: + Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones + + :return: + Returns :class:`dateutil.tz.tzoffset` for offsets and + :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is + specified) offsets equivalent to UTC. + """ + return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc) + + # Constants + _DATE_SEP = b'-' + _TIME_SEP = b':' + _FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)') + + def _parse_isodate(self, dt_str): + try: + return self._parse_isodate_common(dt_str) + except ValueError: + return self._parse_isodate_uncommon(dt_str) + + def _parse_isodate_common(self, dt_str): + len_str = len(dt_str) + components = [1, 1, 1] + + if len_str < 4: + raise ValueError('ISO string too short') + + # Year + components[0] = int(dt_str[0:4]) + pos = 4 + if pos >= len_str: + return components, pos + + has_sep = dt_str[pos:pos + 1] == self._DATE_SEP + if has_sep: + pos += 1 + + # Month + if len_str - pos < 2: + raise ValueError('Invalid common month') + + components[1] = int(dt_str[pos:pos + 2]) + pos += 2 + + if pos >= len_str: + if has_sep: + return components, pos + else: + raise ValueError('Invalid ISO format') + + if has_sep: + if dt_str[pos:pos + 1] != self._DATE_SEP: + raise ValueError('Invalid separator in ISO string') + pos += 1 + + # Day + if len_str - pos < 2: + raise ValueError('Invalid common day') + components[2] = int(dt_str[pos:pos + 2]) + return components, pos + 2 + + def _parse_isodate_uncommon(self, dt_str): + if len(dt_str) < 4: + raise ValueError('ISO string too short') + + # All ISO formats start with the year + year = int(dt_str[0:4]) + + has_sep = dt_str[4:5] == self._DATE_SEP + + pos = 4 + has_sep # Skip '-' if it's there + if dt_str[pos:pos + 1] == b'W': + # YYYY-?Www-?D? + pos += 1 + weekno = int(dt_str[pos:pos + 2]) + pos += 2 + + dayno = 1 + if len(dt_str) > pos: + if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep: + raise ValueError('Inconsistent use of dash separator') + + pos += has_sep + + dayno = int(dt_str[pos:pos + 1]) + pos += 1 + + base_date = self._calculate_weekdate(year, weekno, dayno) + else: + # YYYYDDD or YYYY-DDD + if len(dt_str) - pos < 3: + raise ValueError('Invalid ordinal day') + + ordinal_day = int(dt_str[pos:pos + 3]) + pos += 3 + + if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)): + raise ValueError('Invalid ordinal day' + + ' {} for year {}'.format(ordinal_day, year)) + + base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1) + + components = [base_date.year, base_date.month, base_date.day] + return components, pos + + def _calculate_weekdate(self, year, week, day): + """ + Calculate the day of corresponding to the ISO year-week-day calendar. + + This function is effectively the inverse of + :func:`datetime.date.isocalendar`. + + :param year: + The year in the ISO calendar + + :param week: + The week in the ISO calendar - range is [1, 53] + + :param day: + The day in the ISO calendar - range is [1 (MON), 7 (SUN)] + + :return: + Returns a :class:`datetime.date` + """ + if not 0 < week < 54: + raise ValueError('Invalid week: {}'.format(week)) + + if not 0 < day < 8: # Range is 1-7 + raise ValueError('Invalid weekday: {}'.format(day)) + + # Get week 1 for the specific year: + jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it + week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1) + + # Now add the specific number of weeks and days to get what we want + week_offset = (week - 1) * 7 + (day - 1) + return week_1 + timedelta(days=week_offset) + + def _parse_isotime(self, timestr): + len_str = len(timestr) + components = [0, 0, 0, 0, None] + pos = 0 + comp = -1 + + if len_str < 2: + raise ValueError('ISO time too short') + + has_sep = False + + while pos < len_str and comp < 5: + comp += 1 + + if timestr[pos:pos + 1] in b'-+Zz': + # Detect time zone boundary + components[-1] = self._parse_tzstr(timestr[pos:]) + pos = len_str + break + + if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP: + has_sep = True + pos += 1 + elif comp == 2 and has_sep: + if timestr[pos:pos+1] != self._TIME_SEP: + raise ValueError('Inconsistent use of colon separator') + pos += 1 + + if comp < 3: + # Hour, minute, second + components[comp] = int(timestr[pos:pos + 2]) + pos += 2 + + if comp == 3: + # Fraction of a second + frac = self._FRACTION_REGEX.match(timestr[pos:]) + if not frac: + continue + + us_str = frac.group(1)[:6] # Truncate to microseconds + components[comp] = int(us_str) * 10**(6 - len(us_str)) + pos += len(frac.group()) + + if pos < len_str: + raise ValueError('Unused components in ISO string') + + if components[0] == 24: + # Standard supports 00:00 and 24:00 as representations of midnight + if any(component != 0 for component in components[1:4]): + raise ValueError('Hour may only be 24 at 24:00:00.000') + + return components + + def _parse_tzstr(self, tzstr, zero_as_utc=True): + if tzstr == b'Z' or tzstr == b'z': + return tz.UTC + + if len(tzstr) not in {3, 5, 6}: + raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters') + + if tzstr[0:1] == b'-': + mult = -1 + elif tzstr[0:1] == b'+': + mult = 1 + else: + raise ValueError('Time zone offset requires sign') + + hours = int(tzstr[1:3]) + if len(tzstr) == 3: + minutes = 0 + else: + minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):]) + + if zero_as_utc and hours == 0 and minutes == 0: + return tz.UTC + else: + if minutes > 59: + raise ValueError('Invalid minutes in time zone offset') + + if hours > 23: + raise ValueError('Invalid hours in time zone offset') + + return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60) + + +DEFAULT_ISOPARSER = isoparser() +isoparse = DEFAULT_ISOPARSER.isoparse diff --git a/tapdown/lib/python3.11/site-packages/dateutil/relativedelta.py b/tapdown/lib/python3.11/site-packages/dateutil/relativedelta.py new file mode 100644 index 0000000..cd323a5 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/relativedelta.py @@ -0,0 +1,599 @@ +# -*- coding: utf-8 -*- +import datetime +import calendar + +import operator +from math import copysign + +from six import integer_types +from warnings import warn + +from ._common import weekday + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + +__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + + +class relativedelta(object): + """ + The relativedelta type is designed to be applied to an existing datetime and + can replace specific components of that datetime, or represents an interval + of time. + + It is based on the specification of the excellent work done by M.-A. Lemburg + in his + `mx.DateTime `_ extension. + However, notice that this type does *NOT* implement the same algorithm as + his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. + + There are two different ways to build a relativedelta instance. The + first one is passing it two date/datetime classes:: + + relativedelta(datetime1, datetime2) + + The second one is passing it any number of the following keyword arguments:: + + relativedelta(arg1=x,arg2=y,arg3=z...) + + year, month, day, hour, minute, second, microsecond: + Absolute information (argument is singular); adding or subtracting a + relativedelta with absolute information does not perform an arithmetic + operation, but rather REPLACES the corresponding value in the + original datetime with the value(s) in relativedelta. + + years, months, weeks, days, hours, minutes, seconds, microseconds: + Relative information, may be negative (argument is plural); adding + or subtracting a relativedelta with relative information performs + the corresponding arithmetic operation on the original datetime value + with the information in the relativedelta. + + weekday: + One of the weekday instances (MO, TU, etc) available in the + relativedelta module. These instances may receive a parameter N, + specifying the Nth weekday, which could be positive or negative + (like MO(+1) or MO(-2)). Not specifying it is the same as specifying + +1. You can also use an integer, where 0=MO. This argument is always + relative e.g. if the calculated date is already Monday, using MO(1) + or MO(-1) won't change the day. To effectively make it absolute, use + it in combination with the day argument (e.g. day=1, MO(1) for first + Monday of the month). + + leapdays: + Will add given days to the date found, if year is a leap + year, and the date found is post 28 of february. + + yearday, nlyearday: + Set the yearday or the non-leap year day (jump leap days). + These are converted to day/month/leapdays information. + + There are relative and absolute forms of the keyword + arguments. The plural is relative, and the singular is + absolute. For each argument in the order below, the absolute form + is applied first (by setting each attribute to that value) and + then the relative form (by adding the value to the attribute). + + The order of attributes considered when this relativedelta is + added to a datetime is: + + 1. Year + 2. Month + 3. Day + 4. Hours + 5. Minutes + 6. Seconds + 7. Microseconds + + Finally, weekday is applied, using the rule described above. + + For example + + >>> from datetime import datetime + >>> from dateutil.relativedelta import relativedelta, MO + >>> dt = datetime(2018, 4, 9, 13, 37, 0) + >>> delta = relativedelta(hours=25, day=1, weekday=MO(1)) + >>> dt + delta + datetime.datetime(2018, 4, 2, 14, 37) + + First, the day is set to 1 (the first of the month), then 25 hours + are added, to get to the 2nd day and 14th hour, finally the + weekday is applied, but since the 2nd is already a Monday there is + no effect. + + """ + + def __init__(self, dt1=None, dt2=None, + years=0, months=0, days=0, leapdays=0, weeks=0, + hours=0, minutes=0, seconds=0, microseconds=0, + year=None, month=None, day=None, weekday=None, + yearday=None, nlyearday=None, + hour=None, minute=None, second=None, microsecond=None): + + if dt1 and dt2: + # datetime is a subclass of date. So both must be date + if not (isinstance(dt1, datetime.date) and + isinstance(dt2, datetime.date)): + raise TypeError("relativedelta only diffs datetime/date") + + # We allow two dates, or two datetimes, so we coerce them to be + # of the same type + if (isinstance(dt1, datetime.datetime) != + isinstance(dt2, datetime.datetime)): + if not isinstance(dt1, datetime.datetime): + dt1 = datetime.datetime.fromordinal(dt1.toordinal()) + elif not isinstance(dt2, datetime.datetime): + dt2 = datetime.datetime.fromordinal(dt2.toordinal()) + + self.years = 0 + self.months = 0 + self.days = 0 + self.leapdays = 0 + self.hours = 0 + self.minutes = 0 + self.seconds = 0 + self.microseconds = 0 + self.year = None + self.month = None + self.day = None + self.weekday = None + self.hour = None + self.minute = None + self.second = None + self.microsecond = None + self._has_time = 0 + + # Get year / month delta between the two + months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month) + self._set_months(months) + + # Remove the year/month delta so the timedelta is just well-defined + # time units (seconds, days and microseconds) + dtm = self.__radd__(dt2) + + # If we've overshot our target, make an adjustment + if dt1 < dt2: + compare = operator.gt + increment = 1 + else: + compare = operator.lt + increment = -1 + + while compare(dt1, dtm): + months += increment + self._set_months(months) + dtm = self.__radd__(dt2) + + # Get the timedelta between the "months-adjusted" date and dt1 + delta = dt1 - dtm + self.seconds = delta.seconds + delta.days * 86400 + self.microseconds = delta.microseconds + else: + # Check for non-integer values in integer-only quantities + if any(x is not None and x != int(x) for x in (years, months)): + raise ValueError("Non-integer years and months are " + "ambiguous and not currently supported.") + + # Relative information + self.years = int(years) + self.months = int(months) + self.days = days + weeks * 7 + self.leapdays = leapdays + self.hours = hours + self.minutes = minutes + self.seconds = seconds + self.microseconds = microseconds + + # Absolute information + self.year = year + self.month = month + self.day = day + self.hour = hour + self.minute = minute + self.second = second + self.microsecond = microsecond + + if any(x is not None and int(x) != x + for x in (year, month, day, hour, + minute, second, microsecond)): + # For now we'll deprecate floats - later it'll be an error. + warn("Non-integer value passed as absolute information. " + + "This is not a well-defined condition and will raise " + + "errors in future versions.", DeprecationWarning) + + if isinstance(weekday, integer_types): + self.weekday = weekdays[weekday] + else: + self.weekday = weekday + + yday = 0 + if nlyearday: + yday = nlyearday + elif yearday: + yday = yearday + if yearday > 59: + self.leapdays = -1 + if yday: + ydayidx = [31, 59, 90, 120, 151, 181, 212, + 243, 273, 304, 334, 366] + for idx, ydays in enumerate(ydayidx): + if yday <= ydays: + self.month = idx+1 + if idx == 0: + self.day = yday + else: + self.day = yday-ydayidx[idx-1] + break + else: + raise ValueError("invalid year day (%d)" % yday) + + self._fix() + + def _fix(self): + if abs(self.microseconds) > 999999: + s = _sign(self.microseconds) + div, mod = divmod(self.microseconds * s, 1000000) + self.microseconds = mod * s + self.seconds += div * s + if abs(self.seconds) > 59: + s = _sign(self.seconds) + div, mod = divmod(self.seconds * s, 60) + self.seconds = mod * s + self.minutes += div * s + if abs(self.minutes) > 59: + s = _sign(self.minutes) + div, mod = divmod(self.minutes * s, 60) + self.minutes = mod * s + self.hours += div * s + if abs(self.hours) > 23: + s = _sign(self.hours) + div, mod = divmod(self.hours * s, 24) + self.hours = mod * s + self.days += div * s + if abs(self.months) > 11: + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years += div * s + if (self.hours or self.minutes or self.seconds or self.microseconds + or self.hour is not None or self.minute is not None or + self.second is not None or self.microsecond is not None): + self._has_time = 1 + else: + self._has_time = 0 + + @property + def weeks(self): + return int(self.days / 7.0) + + @weeks.setter + def weeks(self, value): + self.days = self.days - (self.weeks * 7) + value * 7 + + def _set_months(self, months): + self.months = months + if abs(self.months) > 11: + s = _sign(self.months) + div, mod = divmod(self.months * s, 12) + self.months = mod * s + self.years = div * s + else: + self.years = 0 + + def normalized(self): + """ + Return a version of this object represented entirely using integer + values for the relative attributes. + + >>> relativedelta(days=1.5, hours=2).normalized() + relativedelta(days=+1, hours=+14) + + :return: + Returns a :class:`dateutil.relativedelta.relativedelta` object. + """ + # Cascade remainders down (rounding each to roughly nearest microsecond) + days = int(self.days) + + hours_f = round(self.hours + 24 * (self.days - days), 11) + hours = int(hours_f) + + minutes_f = round(self.minutes + 60 * (hours_f - hours), 10) + minutes = int(minutes_f) + + seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8) + seconds = int(seconds_f) + + microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds)) + + # Constructor carries overflow back up with call to _fix() + return self.__class__(years=self.years, months=self.months, + days=days, hours=hours, minutes=minutes, + seconds=seconds, microseconds=microseconds, + leapdays=self.leapdays, year=self.year, + month=self.month, day=self.day, + weekday=self.weekday, hour=self.hour, + minute=self.minute, second=self.second, + microsecond=self.microsecond) + + def __add__(self, other): + if isinstance(other, relativedelta): + return self.__class__(years=other.years + self.years, + months=other.months + self.months, + days=other.days + self.days, + hours=other.hours + self.hours, + minutes=other.minutes + self.minutes, + seconds=other.seconds + self.seconds, + microseconds=(other.microseconds + + self.microseconds), + leapdays=other.leapdays or self.leapdays, + year=(other.year if other.year is not None + else self.year), + month=(other.month if other.month is not None + else self.month), + day=(other.day if other.day is not None + else self.day), + weekday=(other.weekday if other.weekday is not None + else self.weekday), + hour=(other.hour if other.hour is not None + else self.hour), + minute=(other.minute if other.minute is not None + else self.minute), + second=(other.second if other.second is not None + else self.second), + microsecond=(other.microsecond if other.microsecond + is not None else + self.microsecond)) + if isinstance(other, datetime.timedelta): + return self.__class__(years=self.years, + months=self.months, + days=self.days + other.days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds + other.seconds, + microseconds=self.microseconds + other.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + if not isinstance(other, datetime.date): + return NotImplemented + elif self._has_time and not isinstance(other, datetime.datetime): + other = datetime.datetime.fromordinal(other.toordinal()) + year = (self.year or other.year)+self.years + month = self.month or other.month + if self.months: + assert 1 <= abs(self.months) <= 12 + month += self.months + if month > 12: + year += 1 + month -= 12 + elif month < 1: + year -= 1 + month += 12 + day = min(calendar.monthrange(year, month)[1], + self.day or other.day) + repl = {"year": year, "month": month, "day": day} + for attr in ["hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + repl[attr] = value + days = self.days + if self.leapdays and month > 2 and calendar.isleap(year): + days += self.leapdays + ret = (other.replace(**repl) + + datetime.timedelta(days=days, + hours=self.hours, + minutes=self.minutes, + seconds=self.seconds, + microseconds=self.microseconds)) + if self.weekday: + weekday, nth = self.weekday.weekday, self.weekday.n or 1 + jumpdays = (abs(nth) - 1) * 7 + if nth > 0: + jumpdays += (7 - ret.weekday() + weekday) % 7 + else: + jumpdays += (ret.weekday() - weekday) % 7 + jumpdays *= -1 + ret += datetime.timedelta(days=jumpdays) + return ret + + def __radd__(self, other): + return self.__add__(other) + + def __rsub__(self, other): + return self.__neg__().__radd__(other) + + def __sub__(self, other): + if not isinstance(other, relativedelta): + return NotImplemented # In case the other object defines __rsub__ + return self.__class__(years=self.years - other.years, + months=self.months - other.months, + days=self.days - other.days, + hours=self.hours - other.hours, + minutes=self.minutes - other.minutes, + seconds=self.seconds - other.seconds, + microseconds=self.microseconds - other.microseconds, + leapdays=self.leapdays or other.leapdays, + year=(self.year if self.year is not None + else other.year), + month=(self.month if self.month is not None else + other.month), + day=(self.day if self.day is not None else + other.day), + weekday=(self.weekday if self.weekday is not None else + other.weekday), + hour=(self.hour if self.hour is not None else + other.hour), + minute=(self.minute if self.minute is not None else + other.minute), + second=(self.second if self.second is not None else + other.second), + microsecond=(self.microsecond if self.microsecond + is not None else + other.microsecond)) + + def __abs__(self): + return self.__class__(years=abs(self.years), + months=abs(self.months), + days=abs(self.days), + hours=abs(self.hours), + minutes=abs(self.minutes), + seconds=abs(self.seconds), + microseconds=abs(self.microseconds), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __neg__(self): + return self.__class__(years=-self.years, + months=-self.months, + days=-self.days, + hours=-self.hours, + minutes=-self.minutes, + seconds=-self.seconds, + microseconds=-self.microseconds, + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + def __bool__(self): + return not (not self.years and + not self.months and + not self.days and + not self.hours and + not self.minutes and + not self.seconds and + not self.microseconds and + not self.leapdays and + self.year is None and + self.month is None and + self.day is None and + self.weekday is None and + self.hour is None and + self.minute is None and + self.second is None and + self.microsecond is None) + # Compatibility with Python 2.x + __nonzero__ = __bool__ + + def __mul__(self, other): + try: + f = float(other) + except TypeError: + return NotImplemented + + return self.__class__(years=int(self.years * f), + months=int(self.months * f), + days=int(self.days * f), + hours=int(self.hours * f), + minutes=int(self.minutes * f), + seconds=int(self.seconds * f), + microseconds=int(self.microseconds * f), + leapdays=self.leapdays, + year=self.year, + month=self.month, + day=self.day, + weekday=self.weekday, + hour=self.hour, + minute=self.minute, + second=self.second, + microsecond=self.microsecond) + + __rmul__ = __mul__ + + def __eq__(self, other): + if not isinstance(other, relativedelta): + return NotImplemented + if self.weekday or other.weekday: + if not self.weekday or not other.weekday: + return False + if self.weekday.weekday != other.weekday.weekday: + return False + n1, n2 = self.weekday.n, other.weekday.n + if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)): + return False + return (self.years == other.years and + self.months == other.months and + self.days == other.days and + self.hours == other.hours and + self.minutes == other.minutes and + self.seconds == other.seconds and + self.microseconds == other.microseconds and + self.leapdays == other.leapdays and + self.year == other.year and + self.month == other.month and + self.day == other.day and + self.hour == other.hour and + self.minute == other.minute and + self.second == other.second and + self.microsecond == other.microsecond) + + def __hash__(self): + return hash(( + self.weekday, + self.years, + self.months, + self.days, + self.hours, + self.minutes, + self.seconds, + self.microseconds, + self.leapdays, + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.microsecond, + )) + + def __ne__(self, other): + return not self.__eq__(other) + + def __div__(self, other): + try: + reciprocal = 1 / float(other) + except TypeError: + return NotImplemented + + return self.__mul__(reciprocal) + + __truediv__ = __div__ + + def __repr__(self): + l = [] + for attr in ["years", "months", "days", "leapdays", + "hours", "minutes", "seconds", "microseconds"]: + value = getattr(self, attr) + if value: + l.append("{attr}={value:+g}".format(attr=attr, value=value)) + for attr in ["year", "month", "day", "weekday", + "hour", "minute", "second", "microsecond"]: + value = getattr(self, attr) + if value is not None: + l.append("{attr}={value}".format(attr=attr, value=repr(value))) + return "{classname}({attrs})".format(classname=self.__class__.__name__, + attrs=", ".join(l)) + + +def _sign(x): + return int(copysign(1, x)) + +# vim:ts=4:sw=4:et diff --git a/tapdown/lib/python3.11/site-packages/dateutil/rrule.py b/tapdown/lib/python3.11/site-packages/dateutil/rrule.py new file mode 100644 index 0000000..571a0d2 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/rrule.py @@ -0,0 +1,1737 @@ +# -*- coding: utf-8 -*- +""" +The rrule module offers a small, complete, and very fast, implementation of +the recurrence rules documented in the +`iCalendar RFC `_, +including support for caching of results. +""" +import calendar +import datetime +import heapq +import itertools +import re +import sys +from functools import wraps +# For warning about deprecation of until and count +from warnings import warn + +from six import advance_iterator, integer_types + +from six.moves import _thread, range + +from ._common import weekday as weekdaybase + +try: + from math import gcd +except ImportError: + from fractions import gcd + +__all__ = ["rrule", "rruleset", "rrulestr", + "YEARLY", "MONTHLY", "WEEKLY", "DAILY", + "HOURLY", "MINUTELY", "SECONDLY", + "MO", "TU", "WE", "TH", "FR", "SA", "SU"] + +# Every mask is 7 days longer to handle cross-year weekly periods. +M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 + + [7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7) +M365MASK = list(M366MASK) +M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32)) +MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +MDAY365MASK = list(MDAY366MASK) +M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0)) +NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7]) +NMDAY365MASK = list(NMDAY366MASK) +M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366) +M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365) +WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55 +del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31] +MDAY365MASK = tuple(MDAY365MASK) +M365MASK = tuple(M365MASK) + +FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY'] + +(YEARLY, + MONTHLY, + WEEKLY, + DAILY, + HOURLY, + MINUTELY, + SECONDLY) = list(range(7)) + +# Imported on demand. +easter = None +parser = None + + +class weekday(weekdaybase): + """ + This version of weekday does not allow n = 0. + """ + def __init__(self, wkday, n=None): + if n == 0: + raise ValueError("Can't create weekday with n==0") + + super(weekday, self).__init__(wkday, n) + + +MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7)) + + +def _invalidates_cache(f): + """ + Decorator for rruleset methods which may invalidate the + cached length. + """ + @wraps(f) + def inner_func(self, *args, **kwargs): + rv = f(self, *args, **kwargs) + self._invalidate_cache() + return rv + + return inner_func + + +class rrulebase(object): + def __init__(self, cache=False): + if cache: + self._cache = [] + self._cache_lock = _thread.allocate_lock() + self._invalidate_cache() + else: + self._cache = None + self._cache_complete = False + self._len = None + + def __iter__(self): + if self._cache_complete: + return iter(self._cache) + elif self._cache is None: + return self._iter() + else: + return self._iter_cached() + + def _invalidate_cache(self): + if self._cache is not None: + self._cache = [] + self._cache_complete = False + self._cache_gen = self._iter() + + if self._cache_lock.locked(): + self._cache_lock.release() + + self._len = None + + def _iter_cached(self): + i = 0 + gen = self._cache_gen + cache = self._cache + acquire = self._cache_lock.acquire + release = self._cache_lock.release + while gen: + if i == len(cache): + acquire() + if self._cache_complete: + break + try: + for j in range(10): + cache.append(advance_iterator(gen)) + except StopIteration: + self._cache_gen = gen = None + self._cache_complete = True + break + release() + yield cache[i] + i += 1 + while i < self._len: + yield cache[i] + i += 1 + + def __getitem__(self, item): + if self._cache_complete: + return self._cache[item] + elif isinstance(item, slice): + if item.step and item.step < 0: + return list(iter(self))[item] + else: + return list(itertools.islice(self, + item.start or 0, + item.stop or sys.maxsize, + item.step or 1)) + elif item >= 0: + gen = iter(self) + try: + for i in range(item+1): + res = advance_iterator(gen) + except StopIteration: + raise IndexError + return res + else: + return list(iter(self))[item] + + def __contains__(self, item): + if self._cache_complete: + return item in self._cache + else: + for i in self: + if i == item: + return True + elif i > item: + return False + return False + + # __len__() introduces a large performance penalty. + def count(self): + """ Returns the number of recurrences in this set. It will have go + through the whole recurrence, if this hasn't been done before. """ + if self._len is None: + for x in self: + pass + return self._len + + def before(self, dt, inc=False): + """ Returns the last recurrence before the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + last = None + if inc: + for i in gen: + if i > dt: + break + last = i + else: + for i in gen: + if i >= dt: + break + last = i + return last + + def after(self, dt, inc=False): + """ Returns the first recurrence after the given datetime instance. The + inc keyword defines what happens if dt is an occurrence. With + inc=True, if dt itself is an occurrence, it will be returned. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + if inc: + for i in gen: + if i >= dt: + return i + else: + for i in gen: + if i > dt: + return i + return None + + def xafter(self, dt, count=None, inc=False): + """ + Generator which yields up to `count` recurrences after the given + datetime instance, equivalent to `after`. + + :param dt: + The datetime at which to start generating recurrences. + + :param count: + The maximum number of recurrences to generate. If `None` (default), + dates are generated until the recurrence rule is exhausted. + + :param inc: + If `dt` is an instance of the rule and `inc` is `True`, it is + included in the output. + + :yields: Yields a sequence of `datetime` objects. + """ + + if self._cache_complete: + gen = self._cache + else: + gen = self + + # Select the comparison function + if inc: + comp = lambda dc, dtc: dc >= dtc + else: + comp = lambda dc, dtc: dc > dtc + + # Generate dates + n = 0 + for d in gen: + if comp(d, dt): + if count is not None: + n += 1 + if n > count: + break + + yield d + + def between(self, after, before, inc=False, count=1): + """ Returns all the occurrences of the rrule between after and before. + The inc keyword defines what happens if after and/or before are + themselves occurrences. With inc=True, they will be included in the + list, if they are found in the recurrence set. """ + if self._cache_complete: + gen = self._cache + else: + gen = self + started = False + l = [] + if inc: + for i in gen: + if i > before: + break + elif not started: + if i >= after: + started = True + l.append(i) + else: + l.append(i) + else: + for i in gen: + if i >= before: + break + elif not started: + if i > after: + started = True + l.append(i) + else: + l.append(i) + return l + + +class rrule(rrulebase): + """ + That's the base of the rrule operation. It accepts all the keywords + defined in the RFC as its constructor parameters (except byday, + which was renamed to byweekday) and more. The constructor prototype is:: + + rrule(freq) + + Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY, + or SECONDLY. + + .. note:: + Per RFC section 3.3.10, recurrence instances falling on invalid dates + and times are ignored rather than coerced: + + Recurrence rules may generate recurrence instances with an invalid + date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM + on a day where the local time is moved forward by an hour at 1:00 + AM). Such recurrence instances MUST be ignored and MUST NOT be + counted as part of the recurrence set. + + This can lead to possibly surprising behavior when, for example, the + start date occurs at the end of the month: + + >>> from dateutil.rrule import rrule, MONTHLY + >>> from datetime import datetime + >>> start_date = datetime(2014, 12, 31) + >>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date)) + ... # doctest: +NORMALIZE_WHITESPACE + [datetime.datetime(2014, 12, 31, 0, 0), + datetime.datetime(2015, 1, 31, 0, 0), + datetime.datetime(2015, 3, 31, 0, 0), + datetime.datetime(2015, 5, 31, 0, 0)] + + Additionally, it supports the following keyword arguments: + + :param dtstart: + The recurrence start. Besides being the base for the recurrence, + missing parameters in the final recurrence instances will also be + extracted from this date. If not given, datetime.now() will be used + instead. + :param interval: + The interval between each freq iteration. For example, when using + YEARLY, an interval of 2 means once every two years, but with HOURLY, + it means once every two hours. The default interval is 1. + :param wkst: + The week start day. Must be one of the MO, TU, WE constants, or an + integer, specifying the first day of the week. This will affect + recurrences based on weekly periods. The default week start is got + from calendar.firstweekday(), and may be modified by + calendar.setfirstweekday(). + :param count: + If given, this determines how many occurrences will be generated. + + .. note:: + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. + :param until: + If given, this must be a datetime instance specifying the upper-bound + limit of the recurrence. The last recurrence in the rule is the greatest + datetime that is less than or equal to the value specified in the + ``until`` parameter. + + .. note:: + As of version 2.5.0, the use of the keyword ``until`` in conjunction + with ``count`` is deprecated, to make sure ``dateutil`` is fully + compliant with `RFC-5545 Sec. 3.3.10 `_. Therefore, ``until`` and ``count`` + **must not** occur in the same call to ``rrule``. + :param bysetpos: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each given integer will specify an occurrence + number, corresponding to the nth occurrence of the rule inside the + frequency period. For example, a bysetpos of -1 if combined with a + MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will + result in the last work day of every month. + :param bymonth: + If given, it must be either an integer, or a sequence of integers, + meaning the months to apply the recurrence to. + :param bymonthday: + If given, it must be either an integer, or a sequence of integers, + meaning the month days to apply the recurrence to. + :param byyearday: + If given, it must be either an integer, or a sequence of integers, + meaning the year days to apply the recurrence to. + :param byeaster: + If given, it must be either an integer, or a sequence of integers, + positive or negative. Each integer will define an offset from the + Easter Sunday. Passing the offset 0 to byeaster will yield the Easter + Sunday itself. This is an extension to the RFC specification. + :param byweekno: + If given, it must be either an integer, or a sequence of integers, + meaning the week numbers to apply the recurrence to. Week numbers + have the meaning described in ISO8601, that is, the first week of + the year is that containing at least four days of the new year. + :param byweekday: + If given, it must be either an integer (0 == MO), a sequence of + integers, one of the weekday constants (MO, TU, etc), or a sequence + of these constants. When given, these variables will define the + weekdays where the recurrence will be applied. It's also possible to + use an argument n for the weekday instances, which will mean the nth + occurrence of this weekday in the period. For example, with MONTHLY, + or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the + first friday of the month where the recurrence happens. Notice that in + the RFC documentation, this is specified as BYDAY, but was renamed to + avoid the ambiguity of that keyword. + :param byhour: + If given, it must be either an integer, or a sequence of integers, + meaning the hours to apply the recurrence to. + :param byminute: + If given, it must be either an integer, or a sequence of integers, + meaning the minutes to apply the recurrence to. + :param bysecond: + If given, it must be either an integer, or a sequence of integers, + meaning the seconds to apply the recurrence to. + :param cache: + If given, it must be a boolean value specifying to enable or disable + caching of results. If you will use the same rrule instance multiple + times, enabling caching will improve the performance considerably. + """ + def __init__(self, freq, dtstart=None, + interval=1, wkst=None, count=None, until=None, bysetpos=None, + bymonth=None, bymonthday=None, byyearday=None, byeaster=None, + byweekno=None, byweekday=None, + byhour=None, byminute=None, bysecond=None, + cache=False): + super(rrule, self).__init__(cache) + global easter + if not dtstart: + if until and until.tzinfo: + dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0) + else: + dtstart = datetime.datetime.now().replace(microsecond=0) + elif not isinstance(dtstart, datetime.datetime): + dtstart = datetime.datetime.fromordinal(dtstart.toordinal()) + else: + dtstart = dtstart.replace(microsecond=0) + self._dtstart = dtstart + self._tzinfo = dtstart.tzinfo + self._freq = freq + self._interval = interval + self._count = count + + # Cache the original byxxx rules, if they are provided, as the _byxxx + # attributes do not necessarily map to the inputs, and this can be + # a problem in generating the strings. Only store things if they've + # been supplied (the string retrieval will just use .get()) + self._original_rule = {} + + if until and not isinstance(until, datetime.datetime): + until = datetime.datetime.fromordinal(until.toordinal()) + self._until = until + + if self._dtstart and self._until: + if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None): + # According to RFC5545 Section 3.3.10: + # https://tools.ietf.org/html/rfc5545#section-3.3.10 + # + # > If the "DTSTART" property is specified as a date with UTC + # > time or a date with local time and time zone reference, + # > then the UNTIL rule part MUST be specified as a date with + # > UTC time. + raise ValueError( + 'RRULE UNTIL values must be specified in UTC when DTSTART ' + 'is timezone-aware' + ) + + if count is not None and until: + warn("Using both 'count' and 'until' is inconsistent with RFC 5545" + " and has been deprecated in dateutil. Future versions will " + "raise an error.", DeprecationWarning) + + if wkst is None: + self._wkst = calendar.firstweekday() + elif isinstance(wkst, integer_types): + self._wkst = wkst + else: + self._wkst = wkst.weekday + + if bysetpos is None: + self._bysetpos = None + elif isinstance(bysetpos, integer_types): + if bysetpos == 0 or not (-366 <= bysetpos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + self._bysetpos = (bysetpos,) + else: + self._bysetpos = tuple(bysetpos) + for pos in self._bysetpos: + if pos == 0 or not (-366 <= pos <= 366): + raise ValueError("bysetpos must be between 1 and 366, " + "or between -366 and -1") + + if self._bysetpos: + self._original_rule['bysetpos'] = self._bysetpos + + if (byweekno is None and byyearday is None and bymonthday is None and + byweekday is None and byeaster is None): + if freq == YEARLY: + if bymonth is None: + bymonth = dtstart.month + self._original_rule['bymonth'] = None + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == MONTHLY: + bymonthday = dtstart.day + self._original_rule['bymonthday'] = None + elif freq == WEEKLY: + byweekday = dtstart.weekday() + self._original_rule['byweekday'] = None + + # bymonth + if bymonth is None: + self._bymonth = None + else: + if isinstance(bymonth, integer_types): + bymonth = (bymonth,) + + self._bymonth = tuple(sorted(set(bymonth))) + + if 'bymonth' not in self._original_rule: + self._original_rule['bymonth'] = self._bymonth + + # byyearday + if byyearday is None: + self._byyearday = None + else: + if isinstance(byyearday, integer_types): + byyearday = (byyearday,) + + self._byyearday = tuple(sorted(set(byyearday))) + self._original_rule['byyearday'] = self._byyearday + + # byeaster + if byeaster is not None: + if not easter: + from dateutil import easter + if isinstance(byeaster, integer_types): + self._byeaster = (byeaster,) + else: + self._byeaster = tuple(sorted(byeaster)) + + self._original_rule['byeaster'] = self._byeaster + else: + self._byeaster = None + + # bymonthday + if bymonthday is None: + self._bymonthday = () + self._bynmonthday = () + else: + if isinstance(bymonthday, integer_types): + bymonthday = (bymonthday,) + + bymonthday = set(bymonthday) # Ensure it's unique + + self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0)) + self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0)) + + # Storing positive numbers first, then negative numbers + if 'bymonthday' not in self._original_rule: + self._original_rule['bymonthday'] = tuple( + itertools.chain(self._bymonthday, self._bynmonthday)) + + # byweekno + if byweekno is None: + self._byweekno = None + else: + if isinstance(byweekno, integer_types): + byweekno = (byweekno,) + + self._byweekno = tuple(sorted(set(byweekno))) + + self._original_rule['byweekno'] = self._byweekno + + # byweekday / bynweekday + if byweekday is None: + self._byweekday = None + self._bynweekday = None + else: + # If it's one of the valid non-sequence types, convert to a + # single-element sequence before the iterator that builds the + # byweekday set. + if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"): + byweekday = (byweekday,) + + self._byweekday = set() + self._bynweekday = set() + for wday in byweekday: + if isinstance(wday, integer_types): + self._byweekday.add(wday) + elif not wday.n or freq > MONTHLY: + self._byweekday.add(wday.weekday) + else: + self._bynweekday.add((wday.weekday, wday.n)) + + if not self._byweekday: + self._byweekday = None + elif not self._bynweekday: + self._bynweekday = None + + if self._byweekday is not None: + self._byweekday = tuple(sorted(self._byweekday)) + orig_byweekday = [weekday(x) for x in self._byweekday] + else: + orig_byweekday = () + + if self._bynweekday is not None: + self._bynweekday = tuple(sorted(self._bynweekday)) + orig_bynweekday = [weekday(*x) for x in self._bynweekday] + else: + orig_bynweekday = () + + if 'byweekday' not in self._original_rule: + self._original_rule['byweekday'] = tuple(itertools.chain( + orig_byweekday, orig_bynweekday)) + + # byhour + if byhour is None: + if freq < HOURLY: + self._byhour = {dtstart.hour} + else: + self._byhour = None + else: + if isinstance(byhour, integer_types): + byhour = (byhour,) + + if freq == HOURLY: + self._byhour = self.__construct_byset(start=dtstart.hour, + byxxx=byhour, + base=24) + else: + self._byhour = set(byhour) + + self._byhour = tuple(sorted(self._byhour)) + self._original_rule['byhour'] = self._byhour + + # byminute + if byminute is None: + if freq < MINUTELY: + self._byminute = {dtstart.minute} + else: + self._byminute = None + else: + if isinstance(byminute, integer_types): + byminute = (byminute,) + + if freq == MINUTELY: + self._byminute = self.__construct_byset(start=dtstart.minute, + byxxx=byminute, + base=60) + else: + self._byminute = set(byminute) + + self._byminute = tuple(sorted(self._byminute)) + self._original_rule['byminute'] = self._byminute + + # bysecond + if bysecond is None: + if freq < SECONDLY: + self._bysecond = ((dtstart.second,)) + else: + self._bysecond = None + else: + if isinstance(bysecond, integer_types): + bysecond = (bysecond,) + + self._bysecond = set(bysecond) + + if freq == SECONDLY: + self._bysecond = self.__construct_byset(start=dtstart.second, + byxxx=bysecond, + base=60) + else: + self._bysecond = set(bysecond) + + self._bysecond = tuple(sorted(self._bysecond)) + self._original_rule['bysecond'] = self._bysecond + + if self._freq >= HOURLY: + self._timeset = None + else: + self._timeset = [] + for hour in self._byhour: + for minute in self._byminute: + for second in self._bysecond: + self._timeset.append( + datetime.time(hour, minute, second, + tzinfo=self._tzinfo)) + self._timeset.sort() + self._timeset = tuple(self._timeset) + + def __str__(self): + """ + Output a string that would generate this RRULE if passed to rrulestr. + This is mostly compatible with RFC5545, except for the + dateutil-specific extension BYEASTER. + """ + + output = [] + h, m, s = [None] * 3 + if self._dtstart: + output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S')) + h, m, s = self._dtstart.timetuple()[3:6] + + parts = ['FREQ=' + FREQNAMES[self._freq]] + if self._interval != 1: + parts.append('INTERVAL=' + str(self._interval)) + + if self._wkst: + parts.append('WKST=' + repr(weekday(self._wkst))[0:2]) + + if self._count is not None: + parts.append('COUNT=' + str(self._count)) + + if self._until: + parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S')) + + if self._original_rule.get('byweekday') is not None: + # The str() method on weekday objects doesn't generate + # RFC5545-compliant strings, so we should modify that. + original_rule = dict(self._original_rule) + wday_strings = [] + for wday in original_rule['byweekday']: + if wday.n: + wday_strings.append('{n:+d}{wday}'.format( + n=wday.n, + wday=repr(wday)[0:2])) + else: + wday_strings.append(repr(wday)) + + original_rule['byweekday'] = wday_strings + else: + original_rule = self._original_rule + + partfmt = '{name}={vals}' + for name, key in [('BYSETPOS', 'bysetpos'), + ('BYMONTH', 'bymonth'), + ('BYMONTHDAY', 'bymonthday'), + ('BYYEARDAY', 'byyearday'), + ('BYWEEKNO', 'byweekno'), + ('BYDAY', 'byweekday'), + ('BYHOUR', 'byhour'), + ('BYMINUTE', 'byminute'), + ('BYSECOND', 'bysecond'), + ('BYEASTER', 'byeaster')]: + value = original_rule.get(key) + if value: + parts.append(partfmt.format(name=name, vals=(','.join(str(v) + for v in value)))) + + output.append('RRULE:' + ';'.join(parts)) + return '\n'.join(output) + + def replace(self, **kwargs): + """Return new rrule with same attributes except for those attributes given new + values by whichever keyword arguments are specified.""" + new_kwargs = {"interval": self._interval, + "count": self._count, + "dtstart": self._dtstart, + "freq": self._freq, + "until": self._until, + "wkst": self._wkst, + "cache": False if self._cache is None else True } + new_kwargs.update(self._original_rule) + new_kwargs.update(kwargs) + return rrule(**new_kwargs) + + def _iter(self): + year, month, day, hour, minute, second, weekday, yearday, _ = \ + self._dtstart.timetuple() + + # Some local variables to speed things up a bit + freq = self._freq + interval = self._interval + wkst = self._wkst + until = self._until + bymonth = self._bymonth + byweekno = self._byweekno + byyearday = self._byyearday + byweekday = self._byweekday + byeaster = self._byeaster + bymonthday = self._bymonthday + bynmonthday = self._bynmonthday + bysetpos = self._bysetpos + byhour = self._byhour + byminute = self._byminute + bysecond = self._bysecond + + ii = _iterinfo(self) + ii.rebuild(year, month) + + getdayset = {YEARLY: ii.ydayset, + MONTHLY: ii.mdayset, + WEEKLY: ii.wdayset, + DAILY: ii.ddayset, + HOURLY: ii.ddayset, + MINUTELY: ii.ddayset, + SECONDLY: ii.ddayset}[freq] + + if freq < HOURLY: + timeset = self._timeset + else: + gettimeset = {HOURLY: ii.htimeset, + MINUTELY: ii.mtimeset, + SECONDLY: ii.stimeset}[freq] + if ((freq >= HOURLY and + self._byhour and hour not in self._byhour) or + (freq >= MINUTELY and + self._byminute and minute not in self._byminute) or + (freq >= SECONDLY and + self._bysecond and second not in self._bysecond)): + timeset = () + else: + timeset = gettimeset(hour, minute, second) + + total = 0 + count = self._count + while True: + # Get dayset with the right frequency + dayset, start, end = getdayset(year, month, day) + + # Do the "hard" work ;-) + filtered = False + for i in dayset[start:end]: + if ((bymonth and ii.mmask[i] not in bymonth) or + (byweekno and not ii.wnomask[i]) or + (byweekday and ii.wdaymask[i] not in byweekday) or + (ii.nwdaymask and not ii.nwdaymask[i]) or + (byeaster and not ii.eastermask[i]) or + ((bymonthday or bynmonthday) and + ii.mdaymask[i] not in bymonthday and + ii.nmdaymask[i] not in bynmonthday) or + (byyearday and + ((i < ii.yearlen and i+1 not in byyearday and + -ii.yearlen+i not in byyearday) or + (i >= ii.yearlen and i+1-ii.yearlen not in byyearday and + -ii.nextyearlen+i-ii.yearlen not in byyearday)))): + dayset[i] = None + filtered = True + + # Output results + if bysetpos and timeset: + poslist = [] + for pos in bysetpos: + if pos < 0: + daypos, timepos = divmod(pos, len(timeset)) + else: + daypos, timepos = divmod(pos-1, len(timeset)) + try: + i = [x for x in dayset[start:end] + if x is not None][daypos] + time = timeset[timepos] + except IndexError: + pass + else: + date = datetime.date.fromordinal(ii.yearordinal+i) + res = datetime.datetime.combine(date, time) + if res not in poslist: + poslist.append(res) + poslist.sort() + for res in poslist: + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + total += 1 + yield res + else: + for i in dayset[start:end]: + if i is not None: + date = datetime.date.fromordinal(ii.yearordinal + i) + for time in timeset: + res = datetime.datetime.combine(date, time) + if until and res > until: + self._len = total + return + elif res >= self._dtstart: + if count is not None: + count -= 1 + if count < 0: + self._len = total + return + + total += 1 + yield res + + # Handle frequency and interval + fixday = False + if freq == YEARLY: + year += interval + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == MONTHLY: + month += interval + if month > 12: + div, mod = divmod(month, 12) + month = mod + year += div + if month == 0: + month = 12 + year -= 1 + if year > datetime.MAXYEAR: + self._len = total + return + ii.rebuild(year, month) + elif freq == WEEKLY: + if wkst > weekday: + day += -(weekday+1+(6-wkst))+self._interval*7 + else: + day += -(weekday-wkst)+self._interval*7 + weekday = wkst + fixday = True + elif freq == DAILY: + day += interval + fixday = True + elif freq == HOURLY: + if filtered: + # Jump to one iteration before next day + hour += ((23-hour)//interval)*interval + + if byhour: + ndays, hour = self.__mod_distance(value=hour, + byxxx=self._byhour, + base=24) + else: + ndays, hour = divmod(hour+interval, 24) + + if ndays: + day += ndays + fixday = True + + timeset = gettimeset(hour, minute, second) + elif freq == MINUTELY: + if filtered: + # Jump to one iteration before next day + minute += ((1439-(hour*60+minute))//interval)*interval + + valid = False + rep_rate = (24*60) + for j in range(rep_rate // gcd(interval, rep_rate)): + if byminute: + nhours, minute = \ + self.__mod_distance(value=minute, + byxxx=self._byminute, + base=60) + else: + nhours, minute = divmod(minute+interval, 60) + + div, hour = divmod(hour+nhours, 24) + if div: + day += div + fixday = True + filtered = False + + if not byhour or hour in byhour: + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval and ' + + 'byhour resulting in empty rule.') + + timeset = gettimeset(hour, minute, second) + elif freq == SECONDLY: + if filtered: + # Jump to one iteration before next day + second += (((86399 - (hour * 3600 + minute * 60 + second)) + // interval) * interval) + + rep_rate = (24 * 3600) + valid = False + for j in range(0, rep_rate // gcd(interval, rep_rate)): + if bysecond: + nminutes, second = \ + self.__mod_distance(value=second, + byxxx=self._bysecond, + base=60) + else: + nminutes, second = divmod(second+interval, 60) + + div, minute = divmod(minute+nminutes, 60) + if div: + hour += div + div, hour = divmod(hour, 24) + if div: + day += div + fixday = True + + if ((not byhour or hour in byhour) and + (not byminute or minute in byminute) and + (not bysecond or second in bysecond)): + valid = True + break + + if not valid: + raise ValueError('Invalid combination of interval, ' + + 'byhour and byminute resulting in empty' + + ' rule.') + + timeset = gettimeset(hour, minute, second) + + if fixday and day > 28: + daysinmonth = calendar.monthrange(year, month)[1] + if day > daysinmonth: + while day > daysinmonth: + day -= daysinmonth + month += 1 + if month == 13: + month = 1 + year += 1 + if year > datetime.MAXYEAR: + self._len = total + return + daysinmonth = calendar.monthrange(year, month)[1] + ii.rebuild(year, month) + + def __construct_byset(self, start, byxxx, base): + """ + If a `BYXXX` sequence is passed to the constructor at the same level as + `FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some + specifications which cannot be reached given some starting conditions. + + This occurs whenever the interval is not coprime with the base of a + given unit and the difference between the starting position and the + ending position is not coprime with the greatest common denominator + between the interval and the base. For example, with a FREQ of hourly + starting at 17:00 and an interval of 4, the only valid values for + BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not + coprime. + + :param start: + Specifies the starting position. + :param byxxx: + An iterable containing the list of allowed values. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + This does not preserve the type of the iterable, returning a set, since + the values should be unique and the order is irrelevant, this will + speed up later lookups. + + In the event of an empty set, raises a :exception:`ValueError`, as this + results in an empty rrule. + """ + + cset = set() + + # Support a single byxxx value. + if isinstance(byxxx, integer_types): + byxxx = (byxxx, ) + + for num in byxxx: + i_gcd = gcd(self._interval, base) + # Use divmod rather than % because we need to wrap negative nums. + if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0: + cset.add(num) + + if len(cset) == 0: + raise ValueError("Invalid rrule byxxx generates an empty set.") + + return cset + + def __mod_distance(self, value, byxxx, base): + """ + Calculates the next value in a sequence where the `FREQ` parameter is + specified along with a `BYXXX` parameter at the same "level" + (e.g. `HOURLY` specified with `BYHOUR`). + + :param value: + The old value of the component. + :param byxxx: + The `BYXXX` set, which should have been generated by + `rrule._construct_byset`, or something else which checks that a + valid rule is present. + :param base: + The largest allowable value for the specified frequency (e.g. + 24 hours, 60 minutes). + + If a valid value is not found after `base` iterations (the maximum + number before the sequence would start to repeat), this raises a + :exception:`ValueError`, as no valid values were found. + + This returns a tuple of `divmod(n*interval, base)`, where `n` is the + smallest number of `interval` repetitions until the next specified + value in `byxxx` is found. + """ + accumulator = 0 + for ii in range(1, base + 1): + # Using divmod() over % to account for negative intervals + div, value = divmod(value + self._interval, base) + accumulator += div + if value in byxxx: + return (accumulator, value) + + +class _iterinfo(object): + __slots__ = ["rrule", "lastyear", "lastmonth", + "yearlen", "nextyearlen", "yearordinal", "yearweekday", + "mmask", "mrange", "mdaymask", "nmdaymask", + "wdaymask", "wnomask", "nwdaymask", "eastermask"] + + def __init__(self, rrule): + for attr in self.__slots__: + setattr(self, attr, None) + self.rrule = rrule + + def rebuild(self, year, month): + # Every mask is 7 days longer to handle cross-year weekly periods. + rr = self.rrule + if year != self.lastyear: + self.yearlen = 365 + calendar.isleap(year) + self.nextyearlen = 365 + calendar.isleap(year + 1) + firstyday = datetime.date(year, 1, 1) + self.yearordinal = firstyday.toordinal() + self.yearweekday = firstyday.weekday() + + wday = datetime.date(year, 1, 1).weekday() + if self.yearlen == 365: + self.mmask = M365MASK + self.mdaymask = MDAY365MASK + self.nmdaymask = NMDAY365MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M365RANGE + else: + self.mmask = M366MASK + self.mdaymask = MDAY366MASK + self.nmdaymask = NMDAY366MASK + self.wdaymask = WDAYMASK[wday:] + self.mrange = M366RANGE + + if not rr._byweekno: + self.wnomask = None + else: + self.wnomask = [0]*(self.yearlen+7) + # no1wkst = firstwkst = self.wdaymask.index(rr._wkst) + no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7 + if no1wkst >= 4: + no1wkst = 0 + # Number of days in the year, plus the days we got + # from last year. + wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7 + else: + # Number of days in the year, minus the days we + # left in last year. + wyearlen = self.yearlen-no1wkst + div, mod = divmod(wyearlen, 7) + numweeks = div+mod//4 + for n in rr._byweekno: + if n < 0: + n += numweeks+1 + if not (0 < n <= numweeks): + continue + if n > 1: + i = no1wkst+(n-1)*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + else: + i = no1wkst + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if 1 in rr._byweekno: + # Check week number 1 of next year as well + # TODO: Check -numweeks for next year. + i = no1wkst+numweeks*7 + if no1wkst != firstwkst: + i -= 7-firstwkst + if i < self.yearlen: + # If week starts in next year, we + # don't care about it. + for j in range(7): + self.wnomask[i] = 1 + i += 1 + if self.wdaymask[i] == rr._wkst: + break + if no1wkst: + # Check last week number of last year as + # well. If no1wkst is 0, either the year + # started on week start, or week number 1 + # got days from last year, so there are no + # days from last year's last week number in + # this year. + if -1 not in rr._byweekno: + lyearweekday = datetime.date(year-1, 1, 1).weekday() + lno1wkst = (7-lyearweekday+rr._wkst) % 7 + lyearlen = 365+calendar.isleap(year-1) + if lno1wkst >= 4: + lno1wkst = 0 + lnumweeks = 52+(lyearlen + + (lyearweekday-rr._wkst) % 7) % 7//4 + else: + lnumweeks = 52+(self.yearlen-no1wkst) % 7//4 + else: + lnumweeks = -1 + if lnumweeks in rr._byweekno: + for i in range(no1wkst): + self.wnomask[i] = 1 + + if (rr._bynweekday and (month != self.lastmonth or + year != self.lastyear)): + ranges = [] + if rr._freq == YEARLY: + if rr._bymonth: + for month in rr._bymonth: + ranges.append(self.mrange[month-1:month+1]) + else: + ranges = [(0, self.yearlen)] + elif rr._freq == MONTHLY: + ranges = [self.mrange[month-1:month+1]] + if ranges: + # Weekly frequency won't get here, so we may not + # care about cross-year weekly periods. + self.nwdaymask = [0]*self.yearlen + for first, last in ranges: + last -= 1 + for wday, n in rr._bynweekday: + if n < 0: + i = last+(n+1)*7 + i -= (self.wdaymask[i]-wday) % 7 + else: + i = first+(n-1)*7 + i += (7-self.wdaymask[i]+wday) % 7 + if first <= i <= last: + self.nwdaymask[i] = 1 + + if rr._byeaster: + self.eastermask = [0]*(self.yearlen+7) + eyday = easter.easter(year).toordinal()-self.yearordinal + for offset in rr._byeaster: + self.eastermask[eyday+offset] = 1 + + self.lastyear = year + self.lastmonth = month + + def ydayset(self, year, month, day): + return list(range(self.yearlen)), 0, self.yearlen + + def mdayset(self, year, month, day): + dset = [None]*self.yearlen + start, end = self.mrange[month-1:month+1] + for i in range(start, end): + dset[i] = i + return dset, start, end + + def wdayset(self, year, month, day): + # We need to handle cross-year weeks here. + dset = [None]*(self.yearlen+7) + i = datetime.date(year, month, day).toordinal()-self.yearordinal + start = i + for j in range(7): + dset[i] = i + i += 1 + # if (not (0 <= i < self.yearlen) or + # self.wdaymask[i] == self.rrule._wkst): + # This will cross the year boundary, if necessary. + if self.wdaymask[i] == self.rrule._wkst: + break + return dset, start, i + + def ddayset(self, year, month, day): + dset = [None] * self.yearlen + i = datetime.date(year, month, day).toordinal() - self.yearordinal + dset[i] = i + return dset, i, i + 1 + + def htimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for minute in rr._byminute: + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, + tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def mtimeset(self, hour, minute, second): + tset = [] + rr = self.rrule + for second in rr._bysecond: + tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo)) + tset.sort() + return tset + + def stimeset(self, hour, minute, second): + return (datetime.time(hour, minute, second, + tzinfo=self.rrule._tzinfo),) + + +class rruleset(rrulebase): + """ The rruleset type allows more complex recurrence setups, mixing + multiple rules, dates, exclusion rules, and exclusion dates. The type + constructor takes the following keyword arguments: + + :param cache: If True, caching of results will be enabled, improving + performance of multiple queries considerably. """ + + class _genitem(object): + def __init__(self, genlist, gen): + try: + self.dt = advance_iterator(gen) + genlist.append(self) + except StopIteration: + pass + self.genlist = genlist + self.gen = gen + + def __next__(self): + try: + self.dt = advance_iterator(self.gen) + except StopIteration: + if self.genlist[0] is self: + heapq.heappop(self.genlist) + else: + self.genlist.remove(self) + heapq.heapify(self.genlist) + + next = __next__ + + def __lt__(self, other): + return self.dt < other.dt + + def __gt__(self, other): + return self.dt > other.dt + + def __eq__(self, other): + return self.dt == other.dt + + def __ne__(self, other): + return self.dt != other.dt + + def __init__(self, cache=False): + super(rruleset, self).__init__(cache) + self._rrule = [] + self._rdate = [] + self._exrule = [] + self._exdate = [] + + @_invalidates_cache + def rrule(self, rrule): + """ Include the given :py:class:`rrule` instance in the recurrence set + generation. """ + self._rrule.append(rrule) + + @_invalidates_cache + def rdate(self, rdate): + """ Include the given :py:class:`datetime` instance in the recurrence + set generation. """ + self._rdate.append(rdate) + + @_invalidates_cache + def exrule(self, exrule): + """ Include the given rrule instance in the recurrence set exclusion + list. Dates which are part of the given recurrence rules will not + be generated, even if some inclusive rrule or rdate matches them. + """ + self._exrule.append(exrule) + + @_invalidates_cache + def exdate(self, exdate): + """ Include the given datetime instance in the recurrence set + exclusion list. Dates included that way will not be generated, + even if some inclusive rrule or rdate matches them. """ + self._exdate.append(exdate) + + def _iter(self): + rlist = [] + self._rdate.sort() + self._genitem(rlist, iter(self._rdate)) + for gen in [iter(x) for x in self._rrule]: + self._genitem(rlist, gen) + exlist = [] + self._exdate.sort() + self._genitem(exlist, iter(self._exdate)) + for gen in [iter(x) for x in self._exrule]: + self._genitem(exlist, gen) + lastdt = None + total = 0 + heapq.heapify(rlist) + heapq.heapify(exlist) + while rlist: + ritem = rlist[0] + if not lastdt or lastdt != ritem.dt: + while exlist and exlist[0] < ritem: + exitem = exlist[0] + advance_iterator(exitem) + if exlist and exlist[0] is exitem: + heapq.heapreplace(exlist, exitem) + if not exlist or ritem != exlist[0]: + total += 1 + yield ritem.dt + lastdt = ritem.dt + advance_iterator(ritem) + if rlist and rlist[0] is ritem: + heapq.heapreplace(rlist, ritem) + self._len = total + + + + +class _rrulestr(object): + """ Parses a string representation of a recurrence rule or set of + recurrence rules. + + :param s: + Required, a string defining one or more recurrence rules. + + :param dtstart: + If given, used as the default recurrence start if not specified in the + rule string. + + :param cache: + If set ``True`` caching of results will be enabled, improving + performance of multiple queries considerably. + + :param unfold: + If set ``True`` indicates that a rule string is split over more + than one line and should be joined before processing. + + :param forceset: + If set ``True`` forces a :class:`dateutil.rrule.rruleset` to + be returned. + + :param compatible: + If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``. + + :param ignoretz: + If set ``True``, time zones in parsed strings are ignored and a naive + :class:`datetime.datetime` object is returned. + + :param tzids: + If given, a callable or mapping used to retrieve a + :class:`datetime.tzinfo` from a string representation. + Defaults to :func:`dateutil.tz.gettz`. + + :param tzinfos: + Additional time zone names / aliases which may be present in a string + representation. See :func:`dateutil.parser.parse` for more + information. + + :return: + Returns a :class:`dateutil.rrule.rruleset` or + :class:`dateutil.rrule.rrule` + """ + + _freq_map = {"YEARLY": YEARLY, + "MONTHLY": MONTHLY, + "WEEKLY": WEEKLY, + "DAILY": DAILY, + "HOURLY": HOURLY, + "MINUTELY": MINUTELY, + "SECONDLY": SECONDLY} + + _weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3, + "FR": 4, "SA": 5, "SU": 6} + + def _handle_int(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = int(value) + + def _handle_int_list(self, rrkwargs, name, value, **kwargs): + rrkwargs[name.lower()] = [int(x) for x in value.split(',')] + + _handle_INTERVAL = _handle_int + _handle_COUNT = _handle_int + _handle_BYSETPOS = _handle_int_list + _handle_BYMONTH = _handle_int_list + _handle_BYMONTHDAY = _handle_int_list + _handle_BYYEARDAY = _handle_int_list + _handle_BYEASTER = _handle_int_list + _handle_BYWEEKNO = _handle_int_list + _handle_BYHOUR = _handle_int_list + _handle_BYMINUTE = _handle_int_list + _handle_BYSECOND = _handle_int_list + + def _handle_FREQ(self, rrkwargs, name, value, **kwargs): + rrkwargs["freq"] = self._freq_map[value] + + def _handle_UNTIL(self, rrkwargs, name, value, **kwargs): + global parser + if not parser: + from dateutil import parser + try: + rrkwargs["until"] = parser.parse(value, + ignoretz=kwargs.get("ignoretz"), + tzinfos=kwargs.get("tzinfos")) + except ValueError: + raise ValueError("invalid until date") + + def _handle_WKST(self, rrkwargs, name, value, **kwargs): + rrkwargs["wkst"] = self._weekday_map[value] + + def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs): + """ + Two ways to specify this: +1MO or MO(+1) + """ + l = [] + for wday in value.split(','): + if '(' in wday: + # If it's of the form TH(+1), etc. + splt = wday.split('(') + w = splt[0] + n = int(splt[1][:-1]) + elif len(wday): + # If it's of the form +1MO + for i in range(len(wday)): + if wday[i] not in '+-0123456789': + break + n = wday[:i] or None + w = wday[i:] + if n: + n = int(n) + else: + raise ValueError("Invalid (empty) BYDAY specification.") + + l.append(weekdays[self._weekday_map[w]](n)) + rrkwargs["byweekday"] = l + + _handle_BYDAY = _handle_BYWEEKDAY + + def _parse_rfc_rrule(self, line, + dtstart=None, + cache=False, + ignoretz=False, + tzinfos=None): + if line.find(':') != -1: + name, value = line.split(':') + if name != "RRULE": + raise ValueError("unknown parameter name") + else: + value = line + rrkwargs = {} + for pair in value.split(';'): + name, value = pair.split('=') + name = name.upper() + value = value.upper() + try: + getattr(self, "_handle_"+name)(rrkwargs, name, value, + ignoretz=ignoretz, + tzinfos=tzinfos) + except AttributeError: + raise ValueError("unknown parameter '%s'" % name) + except (KeyError, ValueError): + raise ValueError("invalid '%s': %s" % (name, value)) + return rrule(dtstart=dtstart, cache=cache, **rrkwargs) + + def _parse_date_value(self, date_value, parms, rule_tzids, + ignoretz, tzids, tzinfos): + global parser + if not parser: + from dateutil import parser + + datevals = [] + value_found = False + TZID = None + + for parm in parms: + if parm.startswith("TZID="): + try: + tzkey = rule_tzids[parm.split('TZID=')[-1]] + except KeyError: + continue + if tzids is None: + from . import tz + tzlookup = tz.gettz + elif callable(tzids): + tzlookup = tzids + else: + tzlookup = getattr(tzids, 'get', None) + if tzlookup is None: + msg = ('tzids must be a callable, mapping, or None, ' + 'not %s' % tzids) + raise ValueError(msg) + + TZID = tzlookup(tzkey) + continue + + # RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found + # only once. + if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}: + raise ValueError("unsupported parm: " + parm) + else: + if value_found: + msg = ("Duplicate value parameter found in: " + parm) + raise ValueError(msg) + value_found = True + + for datestr in date_value.split(','): + date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos) + if TZID is not None: + if date.tzinfo is None: + date = date.replace(tzinfo=TZID) + else: + raise ValueError('DTSTART/EXDATE specifies multiple timezone') + datevals.append(date) + + return datevals + + def _parse_rfc(self, s, + dtstart=None, + cache=False, + unfold=False, + forceset=False, + compatible=False, + ignoretz=False, + tzids=None, + tzinfos=None): + global parser + if compatible: + forceset = True + unfold = True + + TZID_NAMES = dict(map( + lambda x: (x.upper(), x), + re.findall('TZID=(?P[^:]+):', s) + )) + s = s.upper() + if not s.strip(): + raise ValueError("empty string") + if unfold: + lines = s.splitlines() + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + else: + lines = s.split() + if (not forceset and len(lines) == 1 and (s.find(':') == -1 or + s.startswith('RRULE:'))): + return self._parse_rfc_rrule(lines[0], cache=cache, + dtstart=dtstart, ignoretz=ignoretz, + tzinfos=tzinfos) + else: + rrulevals = [] + rdatevals = [] + exrulevals = [] + exdatevals = [] + for line in lines: + if not line: + continue + if line.find(':') == -1: + name = "RRULE" + value = line + else: + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0] + parms = parms[1:] + if name == "RRULE": + for parm in parms: + raise ValueError("unsupported RRULE parm: "+parm) + rrulevals.append(value) + elif name == "RDATE": + for parm in parms: + if parm != "VALUE=DATE-TIME": + raise ValueError("unsupported RDATE parm: "+parm) + rdatevals.append(value) + elif name == "EXRULE": + for parm in parms: + raise ValueError("unsupported EXRULE parm: "+parm) + exrulevals.append(value) + elif name == "EXDATE": + exdatevals.extend( + self._parse_date_value(value, parms, + TZID_NAMES, ignoretz, + tzids, tzinfos) + ) + elif name == "DTSTART": + dtvals = self._parse_date_value(value, parms, TZID_NAMES, + ignoretz, tzids, tzinfos) + if len(dtvals) != 1: + raise ValueError("Multiple DTSTART values specified:" + + value) + dtstart = dtvals[0] + else: + raise ValueError("unsupported property: "+name) + if (forceset or len(rrulevals) > 1 or rdatevals + or exrulevals or exdatevals): + if not parser and (rdatevals or exdatevals): + from dateutil import parser + rset = rruleset(cache=cache) + for value in rrulevals: + rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in rdatevals: + for datestr in value.split(','): + rset.rdate(parser.parse(datestr, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exrulevals: + rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart, + ignoretz=ignoretz, + tzinfos=tzinfos)) + for value in exdatevals: + rset.exdate(value) + if compatible and dtstart: + rset.rdate(dtstart) + return rset + else: + return self._parse_rfc_rrule(rrulevals[0], + dtstart=dtstart, + cache=cache, + ignoretz=ignoretz, + tzinfos=tzinfos) + + def __call__(self, s, **kwargs): + return self._parse_rfc(s, **kwargs) + + +rrulestr = _rrulestr() + +# vim:ts=4:sw=4:et diff --git a/tapdown/lib/python3.11/site-packages/dateutil/tz/__init__.py b/tapdown/lib/python3.11/site-packages/dateutil/tz/__init__.py new file mode 100644 index 0000000..af1352c --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/tz/__init__.py @@ -0,0 +1,12 @@ +# -*- coding: utf-8 -*- +from .tz import * +from .tz import __doc__ + +__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", + "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz", + "enfold", "datetime_ambiguous", "datetime_exists", + "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"] + + +class DeprecatedTzFormatWarning(Warning): + """Warning raised when time zones are parsed from deprecated formats.""" diff --git a/tapdown/lib/python3.11/site-packages/dateutil/tz/_common.py b/tapdown/lib/python3.11/site-packages/dateutil/tz/_common.py new file mode 100644 index 0000000..e6ac118 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/tz/_common.py @@ -0,0 +1,419 @@ +from six import PY2 + +from functools import wraps + +from datetime import datetime, timedelta, tzinfo + + +ZERO = timedelta(0) + +__all__ = ['tzname_in_python2', 'enfold'] + + +def tzname_in_python2(namefunc): + """Change unicode output into bytestrings in Python 2 + + tzname() API changed in Python 3. It used to return bytes, but was changed + to unicode strings + """ + if PY2: + @wraps(namefunc) + def adjust_encoding(*args, **kwargs): + name = namefunc(*args, **kwargs) + if name is not None: + name = name.encode() + + return name + + return adjust_encoding + else: + return namefunc + + +# The following is adapted from Alexander Belopolsky's tz library +# https://github.com/abalkin/tz +if hasattr(datetime, 'fold'): + # This is the pre-python 3.6 fold situation + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + .. versionadded:: 2.6.0 + """ + return dt.replace(fold=fold) + +else: + class _DatetimeWithFold(datetime): + """ + This is a class designed to provide a PEP 495-compliant interface for + Python versions before 3.6. It is used only for dates in a fold, so + the ``fold`` attribute is fixed at ``1``. + + .. versionadded:: 2.6.0 + """ + __slots__ = () + + def replace(self, *args, **kwargs): + """ + Return a datetime with the same attributes, except for those + attributes given new values by whichever keyword arguments are + specified. Note that tzinfo=None can be specified to create a naive + datetime from an aware datetime with no conversion of date and time + data. + + This is reimplemented in ``_DatetimeWithFold`` because pypy3 will + return a ``datetime.datetime`` even if ``fold`` is unchanged. + """ + argnames = ( + 'year', 'month', 'day', 'hour', 'minute', 'second', + 'microsecond', 'tzinfo' + ) + + for arg, argname in zip(args, argnames): + if argname in kwargs: + raise TypeError('Duplicate argument: {}'.format(argname)) + + kwargs[argname] = arg + + for argname in argnames: + if argname not in kwargs: + kwargs[argname] = getattr(self, argname) + + dt_class = self.__class__ if kwargs.get('fold', 1) else datetime + + return dt_class(**kwargs) + + @property + def fold(self): + return 1 + + def enfold(dt, fold=1): + """ + Provides a unified interface for assigning the ``fold`` attribute to + datetimes both before and after the implementation of PEP-495. + + :param fold: + The value for the ``fold`` attribute in the returned datetime. This + should be either 0 or 1. + + :return: + Returns an object for which ``getattr(dt, 'fold', 0)`` returns + ``fold`` for all versions of Python. In versions prior to + Python 3.6, this is a ``_DatetimeWithFold`` object, which is a + subclass of :py:class:`datetime.datetime` with the ``fold`` + attribute added, if ``fold`` is 1. + + .. versionadded:: 2.6.0 + """ + if getattr(dt, 'fold', 0) == fold: + return dt + + args = dt.timetuple()[:6] + args += (dt.microsecond, dt.tzinfo) + + if fold: + return _DatetimeWithFold(*args) + else: + return datetime(*args) + + +def _validate_fromutc_inputs(f): + """ + The CPython version of ``fromutc`` checks that the input is a ``datetime`` + object and that ``self`` is attached as its ``tzinfo``. + """ + @wraps(f) + def fromutc(self, dt): + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + return f(self, dt) + + return fromutc + + +class _tzinfo(tzinfo): + """ + Base class for all ``dateutil`` ``tzinfo`` objects. + """ + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + + dt = dt.replace(tzinfo=self) + + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None) + + return same_dt and not same_offset + + def _fold_status(self, dt_utc, dt_wall): + """ + Determine the fold status of a "wall" datetime, given a representation + of the same datetime as a (naive) UTC datetime. This is calculated based + on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all + datetimes, and that this offset is the actual number of hours separating + ``dt_utc`` and ``dt_wall``. + + :param dt_utc: + Representation of the datetime as UTC + + :param dt_wall: + Representation of the datetime as "wall time". This parameter must + either have a `fold` attribute or have a fold-naive + :class:`datetime.tzinfo` attached, otherwise the calculation may + fail. + """ + if self.is_ambiguous(dt_wall): + delta_wall = dt_wall - dt_utc + _fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst())) + else: + _fold = 0 + + return _fold + + def _fold(self, dt): + return getattr(dt, 'fold', 0) + + def _fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurrence, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.datetime` object. + """ + + # Re-implement the algorithm from Python's datetime.py + dtoff = dt.utcoffset() + if dtoff is None: + raise ValueError("fromutc() requires a non-None utcoffset() " + "result") + + # The original datetime.py code assumes that `dst()` defaults to + # zero during ambiguous times. PEP 495 inverts this presumption, so + # for pre-PEP 495 versions of python, we need to tweak the algorithm. + dtdst = dt.dst() + if dtdst is None: + raise ValueError("fromutc() requires a non-None dst() result") + delta = dtoff - dtdst + + dt += delta + # Set fold=1 so we can default to being in the fold for + # ambiguous dates. + dtdst = enfold(dt, fold=1).dst() + if dtdst is None: + raise ValueError("fromutc(): dt.dst gave inconsistent " + "results; cannot convert") + return dt + dtdst + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Given a timezone-aware datetime in a given timezone, calculates a + timezone-aware datetime in a new timezone. + + Since this is the one time that we *know* we have an unambiguous + datetime object, we take this opportunity to determine whether the + datetime is ambiguous and in a "fold" state (e.g. if it's the first + occurrence, chronologically, of the ambiguous datetime). + + :param dt: + A timezone-aware :class:`datetime.datetime` object. + """ + dt_wall = self._fromutc(dt) + + # Calculate the fold status given the two datetimes. + _fold = self._fold_status(dt, dt_wall) + + # Set the default fold value for ambiguous dates + return enfold(dt_wall, fold=_fold) + + +class tzrangebase(_tzinfo): + """ + This is an abstract base class for time zones represented by an annual + transition into and out of DST. Child classes should implement the following + methods: + + * ``__init__(self, *args, **kwargs)`` + * ``transitions(self, year)`` - this is expected to return a tuple of + datetimes representing the DST on and off transitions in standard + time. + + A fully initialized ``tzrangebase`` subclass should also provide the + following attributes: + * ``hasdst``: Boolean whether or not the zone uses DST. + * ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects + representing the respective UTC offsets. + * ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short + abbreviations in DST and STD, respectively. + * ``_hasdst``: Whether or not the zone has DST. + + .. versionadded:: 2.6.0 + """ + def __init__(self): + raise NotImplementedError('tzrangebase is an abstract base class') + + def utcoffset(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + isdst = self._isdst(dt) + + if isdst is None: + return None + elif isdst: + return self._dst_base_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + if self._isdst(dt): + return self._dst_abbr + else: + return self._std_abbr + + def fromutc(self, dt): + """ Given a datetime in UTC, return local time """ + if not isinstance(dt, datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # Get transitions - if there are none, fixed offset + transitions = self.transitions(dt.year) + if transitions is None: + return dt + self.utcoffset(dt) + + # Get the transition times in UTC + dston, dstoff = transitions + + dston -= self._std_offset + dstoff -= self._std_offset + + utc_transitions = (dston, dstoff) + dt_utc = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt_utc, utc_transitions) + + if isdst: + dt_wall = dt + self._dst_offset + else: + dt_wall = dt + self._std_offset + + _fold = int(not isdst and self.is_ambiguous(dt_wall)) + + return enfold(dt_wall, fold=_fold) + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if not self.hasdst: + return False + + start, end = self.transitions(dt.year) + + dt = dt.replace(tzinfo=None) + return (end <= dt < end + self._dst_base_offset) + + def _isdst(self, dt): + if not self.hasdst: + return False + elif dt is None: + return None + + transitions = self.transitions(dt.year) + + if transitions is None: + return False + + dt = dt.replace(tzinfo=None) + + isdst = self._naive_isdst(dt, transitions) + + # Handle ambiguous dates + if not isdst and self.is_ambiguous(dt): + return not self._fold(dt) + else: + return isdst + + def _naive_isdst(self, dt, transitions): + dston, dstoff = transitions + + dt = dt.replace(tzinfo=None) + + if dston < dstoff: + isdst = dston <= dt < dstoff + else: + isdst = not dstoff <= dt < dston + + return isdst + + @property + def _dst_base_offset(self): + return self._dst_offset - self._std_offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(...)" % self.__class__.__name__ + + __reduce__ = object.__reduce__ diff --git a/tapdown/lib/python3.11/site-packages/dateutil/tz/_factories.py b/tapdown/lib/python3.11/site-packages/dateutil/tz/_factories.py new file mode 100644 index 0000000..f8a6589 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/tz/_factories.py @@ -0,0 +1,80 @@ +from datetime import timedelta +import weakref +from collections import OrderedDict + +from six.moves import _thread + + +class _TzSingleton(type): + def __init__(cls, *args, **kwargs): + cls.__instance = None + super(_TzSingleton, cls).__init__(*args, **kwargs) + + def __call__(cls): + if cls.__instance is None: + cls.__instance = super(_TzSingleton, cls).__call__() + return cls.__instance + + +class _TzFactory(type): + def instance(cls, *args, **kwargs): + """Alternate constructor that returns a fresh instance""" + return type.__call__(cls, *args, **kwargs) + + +class _TzOffsetFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = weakref.WeakValueDictionary() + cls.__strong_cache = OrderedDict() + cls.__strong_cache_size = 8 + + cls._cache_lock = _thread.allocate_lock() + + def __call__(cls, name, offset): + if isinstance(offset, timedelta): + key = (name, offset.total_seconds()) + else: + key = (name, offset) + + instance = cls.__instances.get(key, None) + if instance is None: + instance = cls.__instances.setdefault(key, + cls.instance(name, offset)) + + # This lock may not be necessary in Python 3. See GH issue #901 + with cls._cache_lock: + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + + return instance + + +class _TzStrFactory(_TzFactory): + def __init__(cls, *args, **kwargs): + cls.__instances = weakref.WeakValueDictionary() + cls.__strong_cache = OrderedDict() + cls.__strong_cache_size = 8 + + cls.__cache_lock = _thread.allocate_lock() + + def __call__(cls, s, posix_offset=False): + key = (s, posix_offset) + instance = cls.__instances.get(key, None) + + if instance is None: + instance = cls.__instances.setdefault(key, + cls.instance(s, posix_offset)) + + # This lock may not be necessary in Python 3. See GH issue #901 + with cls.__cache_lock: + cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance) + + # Remove an item if the strong cache is overpopulated + if len(cls.__strong_cache) > cls.__strong_cache_size: + cls.__strong_cache.popitem(last=False) + + return instance + diff --git a/tapdown/lib/python3.11/site-packages/dateutil/tz/tz.py b/tapdown/lib/python3.11/site-packages/dateutil/tz/tz.py new file mode 100644 index 0000000..6175914 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/tz/tz.py @@ -0,0 +1,1849 @@ +# -*- coding: utf-8 -*- +""" +This module offers timezone implementations subclassing the abstract +:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format +files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`, +etc), TZ environment string (in all known formats), given ranges (with help +from relative deltas), local machine timezone, fixed offset timezone, and UTC +timezone. +""" +import datetime +import struct +import time +import sys +import os +import bisect +import weakref +from collections import OrderedDict + +import six +from six import string_types +from six.moves import _thread +from ._common import tzname_in_python2, _tzinfo +from ._common import tzrangebase, enfold +from ._common import _validate_fromutc_inputs + +from ._factories import _TzSingleton, _TzOffsetFactory +from ._factories import _TzStrFactory +try: + from .win import tzwin, tzwinlocal +except ImportError: + tzwin = tzwinlocal = None + +# For warning about rounding tzinfo +from warnings import warn + +ZERO = datetime.timedelta(0) +EPOCH = datetime.datetime(1970, 1, 1, 0, 0) +EPOCHORDINAL = EPOCH.toordinal() + + +@six.add_metaclass(_TzSingleton) +class tzutc(datetime.tzinfo): + """ + This is a tzinfo object that represents the UTC time zone. + + **Examples:** + + .. doctest:: + + >>> from datetime import * + >>> from dateutil.tz import * + + >>> datetime.now() + datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) + + >>> datetime.now(tzutc()) + datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) + + >>> datetime.now(tzutc()).tzname() + 'UTC' + + .. versionchanged:: 2.7.0 + ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will + always return the same object. + + .. doctest:: + + >>> from dateutil.tz import tzutc, UTC + >>> tzutc() is tzutc() + True + >>> tzutc() is UTC + True + """ + def utcoffset(self, dt): + return ZERO + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return "UTC" + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + + @_validate_fromutc_inputs + def fromutc(self, dt): + """ + Fast track version of fromutc() returns the original ``dt`` object for + any valid :py:class:`datetime.datetime` object. + """ + return dt + + def __eq__(self, other): + if not isinstance(other, (tzutc, tzoffset)): + return NotImplemented + + return (isinstance(other, tzutc) or + (isinstance(other, tzoffset) and other._offset == ZERO)) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +#: Convenience constant providing a :class:`tzutc()` instance +#: +#: .. versionadded:: 2.7.0 +UTC = tzutc() + + +@six.add_metaclass(_TzOffsetFactory) +class tzoffset(datetime.tzinfo): + """ + A simple class for representing a fixed offset from UTC. + + :param name: + The timezone name, to be returned when ``tzname()`` is called. + :param offset: + The time zone offset in seconds, or (since version 2.6.0, represented + as a :py:class:`datetime.timedelta` object). + """ + def __init__(self, name, offset): + self._name = name + + try: + # Allow a timedelta + offset = offset.total_seconds() + except (TypeError, AttributeError): + pass + + self._offset = datetime.timedelta(seconds=_get_supported_offset(offset)) + + def utcoffset(self, dt): + return self._offset + + def dst(self, dt): + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._name + + @_validate_fromutc_inputs + def fromutc(self, dt): + return dt + self._offset + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + return False + + def __eq__(self, other): + if not isinstance(other, tzoffset): + return NotImplemented + + return self._offset == other._offset + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s, %s)" % (self.__class__.__name__, + repr(self._name), + int(self._offset.total_seconds())) + + __reduce__ = object.__reduce__ + + +class tzlocal(_tzinfo): + """ + A :class:`tzinfo` subclass built around the ``time`` timezone functions. + """ + def __init__(self): + super(tzlocal, self).__init__() + + self._std_offset = datetime.timedelta(seconds=-time.timezone) + if time.daylight: + self._dst_offset = datetime.timedelta(seconds=-time.altzone) + else: + self._dst_offset = self._std_offset + + self._dst_saved = self._dst_offset - self._std_offset + self._hasdst = bool(self._dst_saved) + self._tznames = tuple(time.tzname) + + def utcoffset(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset + else: + return self._std_offset + + def dst(self, dt): + if dt is None and self._hasdst: + return None + + if self._isdst(dt): + return self._dst_offset - self._std_offset + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._tznames[self._isdst(dt)] + + def is_ambiguous(self, dt): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + naive_dst = self._naive_is_dst(dt) + return (not naive_dst and + (naive_dst != self._naive_is_dst(dt - self._dst_saved))) + + def _naive_is_dst(self, dt): + timestamp = _datetime_to_timestamp(dt) + return time.localtime(timestamp + time.timezone).tm_isdst + + def _isdst(self, dt, fold_naive=True): + # We can't use mktime here. It is unstable when deciding if + # the hour near to a change is DST or not. + # + # timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour, + # dt.minute, dt.second, dt.weekday(), 0, -1)) + # return time.localtime(timestamp).tm_isdst + # + # The code above yields the following result: + # + # >>> import tz, datetime + # >>> t = tz.tzlocal() + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRST' + # >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname() + # 'BRDT' + # >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname() + # 'BRDT' + # + # Here is a more stable implementation: + # + if not self._hasdst: + return False + + # Check for ambiguous times: + dstval = self._naive_is_dst(dt) + fold = getattr(dt, 'fold', None) + + if self.is_ambiguous(dt): + if fold is not None: + return not self._fold(dt) + else: + return True + + return dstval + + def __eq__(self, other): + if isinstance(other, tzlocal): + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset) + elif isinstance(other, tzutc): + return (not self._hasdst and + self._tznames[0] in {'UTC', 'GMT'} and + self._std_offset == ZERO) + elif isinstance(other, tzoffset): + return (not self._hasdst and + self._tznames[0] == other._name and + self._std_offset == other._offset) + else: + return NotImplemented + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s()" % self.__class__.__name__ + + __reduce__ = object.__reduce__ + + +class _ttinfo(object): + __slots__ = ["offset", "delta", "isdst", "abbr", + "isstd", "isgmt", "dstoffset"] + + def __init__(self): + for attr in self.__slots__: + setattr(self, attr, None) + + def __repr__(self): + l = [] + for attr in self.__slots__: + value = getattr(self, attr) + if value is not None: + l.append("%s=%s" % (attr, repr(value))) + return "%s(%s)" % (self.__class__.__name__, ", ".join(l)) + + def __eq__(self, other): + if not isinstance(other, _ttinfo): + return NotImplemented + + return (self.offset == other.offset and + self.delta == other.delta and + self.isdst == other.isdst and + self.abbr == other.abbr and + self.isstd == other.isstd and + self.isgmt == other.isgmt and + self.dstoffset == other.dstoffset) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __getstate__(self): + state = {} + for name in self.__slots__: + state[name] = getattr(self, name, None) + return state + + def __setstate__(self, state): + for name in self.__slots__: + if name in state: + setattr(self, name, state[name]) + + +class _tzfile(object): + """ + Lightweight class for holding the relevant transition and time zone + information read from binary tzfiles. + """ + attrs = ['trans_list', 'trans_list_utc', 'trans_idx', 'ttinfo_list', + 'ttinfo_std', 'ttinfo_dst', 'ttinfo_before', 'ttinfo_first'] + + def __init__(self, **kwargs): + for attr in self.attrs: + setattr(self, attr, kwargs.get(attr, None)) + + +class tzfile(_tzinfo): + """ + This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)`` + format timezone files to extract current and historical zone information. + + :param fileobj: + This can be an opened file stream or a file name that the time zone + information can be read from. + + :param filename: + This is an optional parameter specifying the source of the time zone + information in the event that ``fileobj`` is a file object. If omitted + and ``fileobj`` is a file stream, this parameter will be set either to + ``fileobj``'s ``name`` attribute or to ``repr(fileobj)``. + + See `Sources for Time Zone and Daylight Saving Time Data + `_ for more information. + Time zone files can be compiled from the `IANA Time Zone database files + `_ with the `zic time zone compiler + `_ + + .. note:: + + Only construct a ``tzfile`` directly if you have a specific timezone + file on disk that you want to read into a Python ``tzinfo`` object. + If you want to get a ``tzfile`` representing a specific IANA zone, + (e.g. ``'America/New_York'``), you should call + :func:`dateutil.tz.gettz` with the zone identifier. + + + **Examples:** + + Using the US Eastern time zone as an example, we can see that a ``tzfile`` + provides time zone information for the standard Daylight Saving offsets: + + .. testsetup:: tzfile + + from dateutil.tz import gettz + from datetime import datetime + + .. doctest:: tzfile + + >>> NYC = gettz('America/New_York') + >>> NYC + tzfile('/usr/share/zoneinfo/America/New_York') + + >>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST + 2016-01-03 00:00:00-05:00 + + >>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT + 2016-07-07 00:00:00-04:00 + + + The ``tzfile`` structure contains a fully history of the time zone, + so historical dates will also have the right offsets. For example, before + the adoption of the UTC standards, New York used local solar mean time: + + .. doctest:: tzfile + + >>> print(datetime(1901, 4, 12, tzinfo=NYC)) # LMT + 1901-04-12 00:00:00-04:56 + + And during World War II, New York was on "Eastern War Time", which was a + state of permanent daylight saving time: + + .. doctest:: tzfile + + >>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT + 1944-02-07 00:00:00-04:00 + + """ + + def __init__(self, fileobj, filename=None): + super(tzfile, self).__init__() + + file_opened_here = False + if isinstance(fileobj, string_types): + self._filename = fileobj + fileobj = open(fileobj, 'rb') + file_opened_here = True + elif filename is not None: + self._filename = filename + elif hasattr(fileobj, "name"): + self._filename = fileobj.name + else: + self._filename = repr(fileobj) + + if fileobj is not None: + if not file_opened_here: + fileobj = _nullcontext(fileobj) + + with fileobj as file_stream: + tzobj = self._read_tzfile(file_stream) + + self._set_tzdata(tzobj) + + def _set_tzdata(self, tzobj): + """ Set the time zone data of this object from a _tzfile object """ + # Copy the relevant attributes over as private attributes + for attr in _tzfile.attrs: + setattr(self, '_' + attr, getattr(tzobj, attr)) + + def _read_tzfile(self, fileobj): + out = _tzfile() + + # From tzfile(5): + # + # The time zone information files used by tzset(3) + # begin with the magic characters "TZif" to identify + # them as time zone information files, followed by + # sixteen bytes reserved for future use, followed by + # six four-byte values of type long, written in a + # ``standard'' byte order (the high-order byte + # of the value is written first). + if fileobj.read(4).decode() != "TZif": + raise ValueError("magic not found") + + fileobj.read(16) + + ( + # The number of UTC/local indicators stored in the file. + ttisgmtcnt, + + # The number of standard/wall indicators stored in the file. + ttisstdcnt, + + # The number of leap seconds for which data is + # stored in the file. + leapcnt, + + # The number of "transition times" for which data + # is stored in the file. + timecnt, + + # The number of "local time types" for which data + # is stored in the file (must not be zero). + typecnt, + + # The number of characters of "time zone + # abbreviation strings" stored in the file. + charcnt, + + ) = struct.unpack(">6l", fileobj.read(24)) + + # The above header is followed by tzh_timecnt four-byte + # values of type long, sorted in ascending order. + # These values are written in ``standard'' byte order. + # Each is used as a transition time (as returned by + # time(2)) at which the rules for computing local time + # change. + + if timecnt: + out.trans_list_utc = list(struct.unpack(">%dl" % timecnt, + fileobj.read(timecnt*4))) + else: + out.trans_list_utc = [] + + # Next come tzh_timecnt one-byte values of type unsigned + # char; each one tells which of the different types of + # ``local time'' types described in the file is associated + # with the same-indexed transition time. These values + # serve as indices into an array of ttinfo structures that + # appears next in the file. + + if timecnt: + out.trans_idx = struct.unpack(">%dB" % timecnt, + fileobj.read(timecnt)) + else: + out.trans_idx = [] + + # Each ttinfo structure is written as a four-byte value + # for tt_gmtoff of type long, in a standard byte + # order, followed by a one-byte value for tt_isdst + # and a one-byte value for tt_abbrind. In each + # structure, tt_gmtoff gives the number of + # seconds to be added to UTC, tt_isdst tells whether + # tm_isdst should be set by localtime(3), and + # tt_abbrind serves as an index into the array of + # time zone abbreviation characters that follow the + # ttinfo structure(s) in the file. + + ttinfo = [] + + for i in range(typecnt): + ttinfo.append(struct.unpack(">lbb", fileobj.read(6))) + + abbr = fileobj.read(charcnt).decode() + + # Then there are tzh_leapcnt pairs of four-byte + # values, written in standard byte order; the + # first value of each pair gives the time (as + # returned by time(2)) at which a leap second + # occurs; the second gives the total number of + # leap seconds to be applied after the given time. + # The pairs of values are sorted in ascending order + # by time. + + # Not used, for now (but seek for correct file position) + if leapcnt: + fileobj.seek(leapcnt * 8, os.SEEK_CUR) + + # Then there are tzh_ttisstdcnt standard/wall + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as standard + # time or wall clock time, and are used when + # a time zone file is used in handling POSIX-style + # time zone environment variables. + + if ttisstdcnt: + isstd = struct.unpack(">%db" % ttisstdcnt, + fileobj.read(ttisstdcnt)) + + # Finally, there are tzh_ttisgmtcnt UTC/local + # indicators, each stored as a one-byte value; + # they tell whether the transition times associated + # with local time types were specified as UTC or + # local time, and are used when a time zone file + # is used in handling POSIX-style time zone envi- + # ronment variables. + + if ttisgmtcnt: + isgmt = struct.unpack(">%db" % ttisgmtcnt, + fileobj.read(ttisgmtcnt)) + + # Build ttinfo list + out.ttinfo_list = [] + for i in range(typecnt): + gmtoff, isdst, abbrind = ttinfo[i] + gmtoff = _get_supported_offset(gmtoff) + tti = _ttinfo() + tti.offset = gmtoff + tti.dstoffset = datetime.timedelta(0) + tti.delta = datetime.timedelta(seconds=gmtoff) + tti.isdst = isdst + tti.abbr = abbr[abbrind:abbr.find('\x00', abbrind)] + tti.isstd = (ttisstdcnt > i and isstd[i] != 0) + tti.isgmt = (ttisgmtcnt > i and isgmt[i] != 0) + out.ttinfo_list.append(tti) + + # Replace ttinfo indexes for ttinfo objects. + out.trans_idx = [out.ttinfo_list[idx] for idx in out.trans_idx] + + # Set standard, dst, and before ttinfos. before will be + # used when a given time is before any transitions, + # and will be set to the first non-dst ttinfo, or to + # the first dst, if all of them are dst. + out.ttinfo_std = None + out.ttinfo_dst = None + out.ttinfo_before = None + if out.ttinfo_list: + if not out.trans_list_utc: + out.ttinfo_std = out.ttinfo_first = out.ttinfo_list[0] + else: + for i in range(timecnt-1, -1, -1): + tti = out.trans_idx[i] + if not out.ttinfo_std and not tti.isdst: + out.ttinfo_std = tti + elif not out.ttinfo_dst and tti.isdst: + out.ttinfo_dst = tti + + if out.ttinfo_std and out.ttinfo_dst: + break + else: + if out.ttinfo_dst and not out.ttinfo_std: + out.ttinfo_std = out.ttinfo_dst + + for tti in out.ttinfo_list: + if not tti.isdst: + out.ttinfo_before = tti + break + else: + out.ttinfo_before = out.ttinfo_list[0] + + # Now fix transition times to become relative to wall time. + # + # I'm not sure about this. In my tests, the tz source file + # is setup to wall time, and in the binary file isstd and + # isgmt are off, so it should be in wall time. OTOH, it's + # always in gmt time. Let me know if you have comments + # about this. + lastdst = None + lastoffset = None + lastdstoffset = None + lastbaseoffset = None + out.trans_list = [] + + for i, tti in enumerate(out.trans_idx): + offset = tti.offset + dstoffset = 0 + + if lastdst is not None: + if tti.isdst: + if not lastdst: + dstoffset = offset - lastoffset + + if not dstoffset and lastdstoffset: + dstoffset = lastdstoffset + + tti.dstoffset = datetime.timedelta(seconds=dstoffset) + lastdstoffset = dstoffset + + # If a time zone changes its base offset during a DST transition, + # then you need to adjust by the previous base offset to get the + # transition time in local time. Otherwise you use the current + # base offset. Ideally, I would have some mathematical proof of + # why this is true, but I haven't really thought about it enough. + baseoffset = offset - dstoffset + adjustment = baseoffset + if (lastbaseoffset is not None and baseoffset != lastbaseoffset + and tti.isdst != lastdst): + # The base DST has changed + adjustment = lastbaseoffset + + lastdst = tti.isdst + lastoffset = offset + lastbaseoffset = baseoffset + + out.trans_list.append(out.trans_list_utc[i] + adjustment) + + out.trans_idx = tuple(out.trans_idx) + out.trans_list = tuple(out.trans_list) + out.trans_list_utc = tuple(out.trans_list_utc) + + return out + + def _find_last_transition(self, dt, in_utc=False): + # If there's no list, there are no transitions to find + if not self._trans_list: + return None + + timestamp = _datetime_to_timestamp(dt) + + # Find where the timestamp fits in the transition list - if the + # timestamp is a transition time, it's part of the "after" period. + trans_list = self._trans_list_utc if in_utc else self._trans_list + idx = bisect.bisect_right(trans_list, timestamp) + + # We want to know when the previous transition was, so subtract off 1 + return idx - 1 + + def _get_ttinfo(self, idx): + # For no list or after the last transition, default to _ttinfo_std + if idx is None or (idx + 1) >= len(self._trans_list): + return self._ttinfo_std + + # If there is a list and the time is before it, return _ttinfo_before + if idx < 0: + return self._ttinfo_before + + return self._trans_idx[idx] + + def _find_ttinfo(self, dt): + idx = self._resolve_ambiguous_time(dt) + + return self._get_ttinfo(idx) + + def fromutc(self, dt): + """ + The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`. + + :param dt: + A :py:class:`datetime.datetime` object. + + :raises TypeError: + Raised if ``dt`` is not a :py:class:`datetime.datetime` object. + + :raises ValueError: + Raised if this is called with a ``dt`` which does not have this + ``tzinfo`` attached. + + :return: + Returns a :py:class:`datetime.datetime` object representing the + wall time in ``self``'s time zone. + """ + # These isinstance checks are in datetime.tzinfo, so we'll preserve + # them, even if we don't care about duck typing. + if not isinstance(dt, datetime.datetime): + raise TypeError("fromutc() requires a datetime argument") + + if dt.tzinfo is not self: + raise ValueError("dt.tzinfo is not self") + + # First treat UTC as wall time and get the transition we're in. + idx = self._find_last_transition(dt, in_utc=True) + tti = self._get_ttinfo(idx) + + dt_out = dt + datetime.timedelta(seconds=tti.offset) + + fold = self.is_ambiguous(dt_out, idx=idx) + + return enfold(dt_out, fold=int(fold)) + + def is_ambiguous(self, dt, idx=None): + """ + Whether or not the "wall time" of a given datetime is ambiguous in this + zone. + + :param dt: + A :py:class:`datetime.datetime`, naive or time zone aware. + + + :return: + Returns ``True`` if ambiguous, ``False`` otherwise. + + .. versionadded:: 2.6.0 + """ + if idx is None: + idx = self._find_last_transition(dt) + + # Calculate the difference in offsets from current to previous + timestamp = _datetime_to_timestamp(dt) + tti = self._get_ttinfo(idx) + + if idx is None or idx <= 0: + return False + + od = self._get_ttinfo(idx - 1).offset - tti.offset + tt = self._trans_list[idx] # Transition time + + return timestamp < tt + od + + def _resolve_ambiguous_time(self, dt): + idx = self._find_last_transition(dt) + + # If we have no transitions, return the index + _fold = self._fold(dt) + if idx is None or idx == 0: + return idx + + # If it's ambiguous and we're in a fold, shift to a different index. + idx_offset = int(not _fold and self.is_ambiguous(dt, idx)) + + return idx - idx_offset + + def utcoffset(self, dt): + if dt is None: + return None + + if not self._ttinfo_std: + return ZERO + + return self._find_ttinfo(dt).delta + + def dst(self, dt): + if dt is None: + return None + + if not self._ttinfo_dst: + return ZERO + + tti = self._find_ttinfo(dt) + + if not tti.isdst: + return ZERO + + # The documentation says that utcoffset()-dst() must + # be constant for every dt. + return tti.dstoffset + + @tzname_in_python2 + def tzname(self, dt): + if not self._ttinfo_std or dt is None: + return None + return self._find_ttinfo(dt).abbr + + def __eq__(self, other): + if not isinstance(other, tzfile): + return NotImplemented + return (self._trans_list == other._trans_list and + self._trans_idx == other._trans_idx and + self._ttinfo_list == other._ttinfo_list) + + __hash__ = None + + def __ne__(self, other): + return not (self == other) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._filename)) + + def __reduce__(self): + return self.__reduce_ex__(None) + + def __reduce_ex__(self, protocol): + return (self.__class__, (None, self._filename), self.__dict__) + + +class tzrange(tzrangebase): + """ + The ``tzrange`` object is a time zone specified by a set of offsets and + abbreviations, equivalent to the way the ``TZ`` variable can be specified + in POSIX-like systems, but using Python delta objects to specify DST + start, end and offsets. + + :param stdabbr: + The abbreviation for standard time (e.g. ``'EST'``). + + :param stdoffset: + An integer or :class:`datetime.timedelta` object or equivalent + specifying the base offset from UTC. + + If unspecified, +00:00 is used. + + :param dstabbr: + The abbreviation for DST / "Summer" time (e.g. ``'EDT'``). + + If specified, with no other DST information, DST is assumed to occur + and the default behavior or ``dstoffset``, ``start`` and ``end`` is + used. If unspecified and no other DST information is specified, it + is assumed that this zone has no DST. + + If this is unspecified and other DST information is *is* specified, + DST occurs in the zone but the time zone abbreviation is left + unchanged. + + :param dstoffset: + A an integer or :class:`datetime.timedelta` object or equivalent + specifying the UTC offset during DST. If unspecified and any other DST + information is specified, it is assumed to be the STD offset +1 hour. + + :param start: + A :class:`relativedelta.relativedelta` object or equivalent specifying + the time and time of year that daylight savings time starts. To + specify, for example, that DST starts at 2AM on the 2nd Sunday in + March, pass: + + ``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))`` + + If unspecified and any other DST information is specified, the default + value is 2 AM on the first Sunday in April. + + :param end: + A :class:`relativedelta.relativedelta` object or equivalent + representing the time and time of year that daylight savings time + ends, with the same specification method as in ``start``. One note is + that this should point to the first time in the *standard* zone, so if + a transition occurs at 2AM in the DST zone and the clocks are set back + 1 hour to 1AM, set the ``hours`` parameter to +1. + + + **Examples:** + + .. testsetup:: tzrange + + from dateutil.tz import tzrange, tzstr + + .. doctest:: tzrange + + >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") + True + + >>> from dateutil.relativedelta import * + >>> range1 = tzrange("EST", -18000, "EDT") + >>> range2 = tzrange("EST", -18000, "EDT", -14400, + ... relativedelta(hours=+2, month=4, day=1, + ... weekday=SU(+1)), + ... relativedelta(hours=+1, month=10, day=31, + ... weekday=SU(-1))) + >>> tzstr('EST5EDT') == range1 == range2 + True + + """ + def __init__(self, stdabbr, stdoffset=None, + dstabbr=None, dstoffset=None, + start=None, end=None): + + global relativedelta + from dateutil import relativedelta + + self._std_abbr = stdabbr + self._dst_abbr = dstabbr + + try: + stdoffset = stdoffset.total_seconds() + except (TypeError, AttributeError): + pass + + try: + dstoffset = dstoffset.total_seconds() + except (TypeError, AttributeError): + pass + + if stdoffset is not None: + self._std_offset = datetime.timedelta(seconds=stdoffset) + else: + self._std_offset = ZERO + + if dstoffset is not None: + self._dst_offset = datetime.timedelta(seconds=dstoffset) + elif dstabbr and stdoffset is not None: + self._dst_offset = self._std_offset + datetime.timedelta(hours=+1) + else: + self._dst_offset = ZERO + + if dstabbr and start is None: + self._start_delta = relativedelta.relativedelta( + hours=+2, month=4, day=1, weekday=relativedelta.SU(+1)) + else: + self._start_delta = start + + if dstabbr and end is None: + self._end_delta = relativedelta.relativedelta( + hours=+1, month=10, day=31, weekday=relativedelta.SU(-1)) + else: + self._end_delta = end + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = bool(self._start_delta) + + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + if not self.hasdst: + return None + + base_year = datetime.datetime(year, 1, 1) + + start = base_year + self._start_delta + end = base_year + self._end_delta + + return (start, end) + + def __eq__(self, other): + if not isinstance(other, tzrange): + return NotImplemented + + return (self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr and + self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._start_delta == other._start_delta and + self._end_delta == other._end_delta) + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +@six.add_metaclass(_TzStrFactory) +class tzstr(tzrange): + """ + ``tzstr`` objects are time zone objects specified by a time-zone string as + it would be passed to a ``TZ`` variable on POSIX-style systems (see + the `GNU C Library: TZ Variable`_ for more details). + + There is one notable exception, which is that POSIX-style time zones use an + inverted offset format, so normally ``GMT+3`` would be parsed as an offset + 3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an + offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX + behavior, pass a ``True`` value to ``posix_offset``. + + The :class:`tzrange` object provides the same functionality, but is + specified using :class:`relativedelta.relativedelta` objects. rather than + strings. + + :param s: + A time zone string in ``TZ`` variable format. This can be a + :class:`bytes` (2.x: :class:`str`), :class:`str` (2.x: + :class:`unicode`) or a stream emitting unicode characters + (e.g. :class:`StringIO`). + + :param posix_offset: + Optional. If set to ``True``, interpret strings such as ``GMT+3`` or + ``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the + POSIX standard. + + .. caution:: + + Prior to version 2.7.0, this function also supported time zones + in the format: + + * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` + * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` + + This format is non-standard and has been deprecated; this function + will raise a :class:`DeprecatedTZFormatWarning` until + support is removed in a future version. + + .. _`GNU C Library: TZ Variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + """ + def __init__(self, s, posix_offset=False): + global parser + from dateutil.parser import _parser as parser + + self._s = s + + res = parser._parsetz(s) + if res is None or res.any_unused_tokens: + raise ValueError("unknown string format") + + # Here we break the compatibility with the TZ variable handling. + # GMT-3 actually *means* the timezone -3. + if res.stdabbr in ("GMT", "UTC") and not posix_offset: + res.stdoffset *= -1 + + # We must initialize it first, since _delta() needs + # _std_offset and _dst_offset set. Use False in start/end + # to avoid building it two times. + tzrange.__init__(self, res.stdabbr, res.stdoffset, + res.dstabbr, res.dstoffset, + start=False, end=False) + + if not res.dstabbr: + self._start_delta = None + self._end_delta = None + else: + self._start_delta = self._delta(res.start) + if self._start_delta: + self._end_delta = self._delta(res.end, isend=1) + + self.hasdst = bool(self._start_delta) + + def _delta(self, x, isend=0): + from dateutil import relativedelta + kwargs = {} + if x.month is not None: + kwargs["month"] = x.month + if x.weekday is not None: + kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week) + if x.week > 0: + kwargs["day"] = 1 + else: + kwargs["day"] = 31 + elif x.day: + kwargs["day"] = x.day + elif x.yday is not None: + kwargs["yearday"] = x.yday + elif x.jyday is not None: + kwargs["nlyearday"] = x.jyday + if not kwargs: + # Default is to start on first sunday of april, and end + # on last sunday of october. + if not isend: + kwargs["month"] = 4 + kwargs["day"] = 1 + kwargs["weekday"] = relativedelta.SU(+1) + else: + kwargs["month"] = 10 + kwargs["day"] = 31 + kwargs["weekday"] = relativedelta.SU(-1) + if x.time is not None: + kwargs["seconds"] = x.time + else: + # Default is 2AM. + kwargs["seconds"] = 7200 + if isend: + # Convert to standard time, to follow the documented way + # of working with the extra hour. See the documentation + # of the tzinfo class. + delta = self._dst_offset - self._std_offset + kwargs["seconds"] -= delta.seconds + delta.days * 86400 + return relativedelta.relativedelta(**kwargs) + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +class _tzicalvtzcomp(object): + def __init__(self, tzoffsetfrom, tzoffsetto, isdst, + tzname=None, rrule=None): + self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom) + self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto) + self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom + self.isdst = isdst + self.tzname = tzname + self.rrule = rrule + + +class _tzicalvtz(_tzinfo): + def __init__(self, tzid, comps=[]): + super(_tzicalvtz, self).__init__() + + self._tzid = tzid + self._comps = comps + self._cachedate = [] + self._cachecomp = [] + self._cache_lock = _thread.allocate_lock() + + def _find_comp(self, dt): + if len(self._comps) == 1: + return self._comps[0] + + dt = dt.replace(tzinfo=None) + + try: + with self._cache_lock: + return self._cachecomp[self._cachedate.index( + (dt, self._fold(dt)))] + except ValueError: + pass + + lastcompdt = None + lastcomp = None + + for comp in self._comps: + compdt = self._find_compdt(comp, dt) + + if compdt and (not lastcompdt or lastcompdt < compdt): + lastcompdt = compdt + lastcomp = comp + + if not lastcomp: + # RFC says nothing about what to do when a given + # time is before the first onset date. We'll look for the + # first standard component, or the first component, if + # none is found. + for comp in self._comps: + if not comp.isdst: + lastcomp = comp + break + else: + lastcomp = comp[0] + + with self._cache_lock: + self._cachedate.insert(0, (dt, self._fold(dt))) + self._cachecomp.insert(0, lastcomp) + + if len(self._cachedate) > 10: + self._cachedate.pop() + self._cachecomp.pop() + + return lastcomp + + def _find_compdt(self, comp, dt): + if comp.tzoffsetdiff < ZERO and self._fold(dt): + dt -= comp.tzoffsetdiff + + compdt = comp.rrule.before(dt, inc=True) + + return compdt + + def utcoffset(self, dt): + if dt is None: + return None + + return self._find_comp(dt).tzoffsetto + + def dst(self, dt): + comp = self._find_comp(dt) + if comp.isdst: + return comp.tzoffsetdiff + else: + return ZERO + + @tzname_in_python2 + def tzname(self, dt): + return self._find_comp(dt).tzname + + def __repr__(self): + return "" % repr(self._tzid) + + __reduce__ = object.__reduce__ + + +class tzical(object): + """ + This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure + as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. + + :param `fileobj`: + A file or stream in iCalendar format, which should be UTF-8 encoded + with CRLF endings. + + .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 + """ + def __init__(self, fileobj): + global rrule + from dateutil import rrule + + if isinstance(fileobj, string_types): + self._s = fileobj + # ical should be encoded in UTF-8 with CRLF + fileobj = open(fileobj, 'r') + else: + self._s = getattr(fileobj, 'name', repr(fileobj)) + fileobj = _nullcontext(fileobj) + + self._vtz = {} + + with fileobj as fobj: + self._parse_rfc(fobj.read()) + + def keys(self): + """ + Retrieves the available time zones as a list. + """ + return list(self._vtz.keys()) + + def get(self, tzid=None): + """ + Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``. + + :param tzid: + If there is exactly one time zone available, omitting ``tzid`` + or passing :py:const:`None` value returns it. Otherwise a valid + key (which can be retrieved from :func:`keys`) is required. + + :raises ValueError: + Raised if ``tzid`` is not specified but there are either more + or fewer than 1 zone defined. + + :returns: + Returns either a :py:class:`datetime.tzinfo` object representing + the relevant time zone or :py:const:`None` if the ``tzid`` was + not found. + """ + if tzid is None: + if len(self._vtz) == 0: + raise ValueError("no timezones defined") + elif len(self._vtz) > 1: + raise ValueError("more than one timezone available") + tzid = next(iter(self._vtz)) + + return self._vtz.get(tzid) + + def _parse_offset(self, s): + s = s.strip() + if not s: + raise ValueError("empty offset") + if s[0] in ('+', '-'): + signal = (-1, +1)[s[0] == '+'] + s = s[1:] + else: + signal = +1 + if len(s) == 4: + return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal + elif len(s) == 6: + return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal + else: + raise ValueError("invalid offset: " + s) + + def _parse_rfc(self, s): + lines = s.splitlines() + if not lines: + raise ValueError("empty string") + + # Unfold + i = 0 + while i < len(lines): + line = lines[i].rstrip() + if not line: + del lines[i] + elif i > 0 and line[0] == " ": + lines[i-1] += line[1:] + del lines[i] + else: + i += 1 + + tzid = None + comps = [] + invtz = False + comptype = None + for line in lines: + if not line: + continue + name, value = line.split(':', 1) + parms = name.split(';') + if not parms: + raise ValueError("empty property name") + name = parms[0].upper() + parms = parms[1:] + if invtz: + if name == "BEGIN": + if value in ("STANDARD", "DAYLIGHT"): + # Process component + pass + else: + raise ValueError("unknown component: "+value) + comptype = value + founddtstart = False + tzoffsetfrom = None + tzoffsetto = None + rrulelines = [] + tzname = None + elif name == "END": + if value == "VTIMEZONE": + if comptype: + raise ValueError("component not closed: "+comptype) + if not tzid: + raise ValueError("mandatory TZID not found") + if not comps: + raise ValueError( + "at least one component is needed") + # Process vtimezone + self._vtz[tzid] = _tzicalvtz(tzid, comps) + invtz = False + elif value == comptype: + if not founddtstart: + raise ValueError("mandatory DTSTART not found") + if tzoffsetfrom is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + if tzoffsetto is None: + raise ValueError( + "mandatory TZOFFSETFROM not found") + # Process component + rr = None + if rrulelines: + rr = rrule.rrulestr("\n".join(rrulelines), + compatible=True, + ignoretz=True, + cache=True) + comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto, + (comptype == "DAYLIGHT"), + tzname, rr) + comps.append(comp) + comptype = None + else: + raise ValueError("invalid component end: "+value) + elif comptype: + if name == "DTSTART": + # DTSTART in VTIMEZONE takes a subset of valid RRULE + # values under RFC 5545. + for parm in parms: + if parm != 'VALUE=DATE-TIME': + msg = ('Unsupported DTSTART param in ' + + 'VTIMEZONE: ' + parm) + raise ValueError(msg) + rrulelines.append(line) + founddtstart = True + elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"): + rrulelines.append(line) + elif name == "TZOFFSETFROM": + if parms: + raise ValueError( + "unsupported %s parm: %s " % (name, parms[0])) + tzoffsetfrom = self._parse_offset(value) + elif name == "TZOFFSETTO": + if parms: + raise ValueError( + "unsupported TZOFFSETTO parm: "+parms[0]) + tzoffsetto = self._parse_offset(value) + elif name == "TZNAME": + if parms: + raise ValueError( + "unsupported TZNAME parm: "+parms[0]) + tzname = value + elif name == "COMMENT": + pass + else: + raise ValueError("unsupported property: "+name) + else: + if name == "TZID": + if parms: + raise ValueError( + "unsupported TZID parm: "+parms[0]) + tzid = value + elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"): + pass + else: + raise ValueError("unsupported property: "+name) + elif name == "BEGIN" and value == "VTIMEZONE": + tzid = None + comps = [] + invtz = True + + def __repr__(self): + return "%s(%s)" % (self.__class__.__name__, repr(self._s)) + + +if sys.platform != "win32": + TZFILES = ["/etc/localtime", "localtime"] + TZPATHS = ["/usr/share/zoneinfo", + "/usr/lib/zoneinfo", + "/usr/share/lib/zoneinfo", + "/etc/zoneinfo"] +else: + TZFILES = [] + TZPATHS = [] + + +def __get_gettz(): + tzlocal_classes = (tzlocal,) + if tzwinlocal is not None: + tzlocal_classes += (tzwinlocal,) + + class GettzFunc(object): + """ + Retrieve a time zone object from a string representation + + This function is intended to retrieve the :py:class:`tzinfo` subclass + that best represents the time zone that would be used if a POSIX + `TZ variable`_ were set to the same value. + + If no argument or an empty string is passed to ``gettz``, local time + is returned: + + .. code-block:: python3 + + >>> gettz() + tzfile('/etc/localtime') + + This function is also the preferred way to map IANA tz database keys + to :class:`tzfile` objects: + + .. code-block:: python3 + + >>> gettz('Pacific/Kiritimati') + tzfile('/usr/share/zoneinfo/Pacific/Kiritimati') + + On Windows, the standard is extended to include the Windows-specific + zone names provided by the operating system: + + .. code-block:: python3 + + >>> gettz('Egypt Standard Time') + tzwin('Egypt Standard Time') + + Passing a GNU ``TZ`` style string time zone specification returns a + :class:`tzstr` object: + + .. code-block:: python3 + + >>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3') + + :param name: + A time zone name (IANA, or, on Windows, Windows keys), location of + a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone + specifier. An empty string, no argument or ``None`` is interpreted + as local time. + + :return: + Returns an instance of one of ``dateutil``'s :py:class:`tzinfo` + subclasses. + + .. versionchanged:: 2.7.0 + + After version 2.7.0, any two calls to ``gettz`` using the same + input strings will return the same object: + + .. code-block:: python3 + + >>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago') + True + + In addition to improving performance, this ensures that + `"same zone" semantics`_ are used for datetimes in the same zone. + + + .. _`TZ variable`: + https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html + + .. _`"same zone" semantics`: + https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html + """ + def __init__(self): + + self.__instances = weakref.WeakValueDictionary() + self.__strong_cache_size = 8 + self.__strong_cache = OrderedDict() + self._cache_lock = _thread.allocate_lock() + + def __call__(self, name=None): + with self._cache_lock: + rv = self.__instances.get(name, None) + + if rv is None: + rv = self.nocache(name=name) + if not (name is None + or isinstance(rv, tzlocal_classes) + or rv is None): + # tzlocal is slightly more complicated than the other + # time zone providers because it depends on environment + # at construction time, so don't cache that. + # + # We also cannot store weak references to None, so we + # will also not store that. + self.__instances[name] = rv + else: + # No need for strong caching, return immediately + return rv + + self.__strong_cache[name] = self.__strong_cache.pop(name, rv) + + if len(self.__strong_cache) > self.__strong_cache_size: + self.__strong_cache.popitem(last=False) + + return rv + + def set_cache_size(self, size): + with self._cache_lock: + self.__strong_cache_size = size + while len(self.__strong_cache) > size: + self.__strong_cache.popitem(last=False) + + def cache_clear(self): + with self._cache_lock: + self.__instances = weakref.WeakValueDictionary() + self.__strong_cache.clear() + + @staticmethod + def nocache(name=None): + """A non-cached version of gettz""" + tz = None + if not name: + try: + name = os.environ["TZ"] + except KeyError: + pass + if name is None or name in ("", ":"): + for filepath in TZFILES: + if not os.path.isabs(filepath): + filename = filepath + for path in TZPATHS: + filepath = os.path.join(path, filename) + if os.path.isfile(filepath): + break + else: + continue + if os.path.isfile(filepath): + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = tzlocal() + else: + try: + if name.startswith(":"): + name = name[1:] + except TypeError as e: + if isinstance(name, bytes): + new_msg = "gettz argument should be str, not bytes" + six.raise_from(TypeError(new_msg), e) + else: + raise + if os.path.isabs(name): + if os.path.isfile(name): + tz = tzfile(name) + else: + tz = None + else: + for path in TZPATHS: + filepath = os.path.join(path, name) + if not os.path.isfile(filepath): + filepath = filepath.replace(' ', '_') + if not os.path.isfile(filepath): + continue + try: + tz = tzfile(filepath) + break + except (IOError, OSError, ValueError): + pass + else: + tz = None + if tzwin is not None: + try: + tz = tzwin(name) + except (WindowsError, UnicodeEncodeError): + # UnicodeEncodeError is for Python 2.7 compat + tz = None + + if not tz: + from dateutil.zoneinfo import get_zonefile_instance + tz = get_zonefile_instance().get(name) + + if not tz: + for c in name: + # name is not a tzstr unless it has at least + # one offset. For short values of "name", an + # explicit for loop seems to be the fastest way + # To determine if a string contains a digit + if c in "0123456789": + try: + tz = tzstr(name) + except ValueError: + pass + break + else: + if name in ("GMT", "UTC"): + tz = UTC + elif name in time.tzname: + tz = tzlocal() + return tz + + return GettzFunc() + + +gettz = __get_gettz() +del __get_gettz + + +def datetime_exists(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + would fall in a gap. + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" exists in + ``tz``. + + .. versionadded:: 2.7.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + tz = dt.tzinfo + + dt = dt.replace(tzinfo=None) + + # This is essentially a test of whether or not the datetime can survive + # a round trip to UTC. + dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz) + dt_rt = dt_rt.replace(tzinfo=None) + + return dt == dt_rt + + +def datetime_ambiguous(dt, tz=None): + """ + Given a datetime and a time zone, determine whether or not a given datetime + is ambiguous (i.e if there are two times differentiated only by their DST + status). + + :param dt: + A :class:`datetime.datetime` (whose time zone will be ignored if ``tz`` + is provided.) + + :param tz: + A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If + ``None`` or not provided, the datetime's own time zone will be used. + + :return: + Returns a boolean value whether or not the "wall time" is ambiguous in + ``tz``. + + .. versionadded:: 2.6.0 + """ + if tz is None: + if dt.tzinfo is None: + raise ValueError('Datetime is naive and no time zone provided.') + + tz = dt.tzinfo + + # If a time zone defines its own "is_ambiguous" function, we'll use that. + is_ambiguous_fn = getattr(tz, 'is_ambiguous', None) + if is_ambiguous_fn is not None: + try: + return tz.is_ambiguous(dt) + except Exception: + pass + + # If it doesn't come out and tell us it's ambiguous, we'll just check if + # the fold attribute has any effect on this particular date and time. + dt = dt.replace(tzinfo=tz) + wall_0 = enfold(dt, fold=0) + wall_1 = enfold(dt, fold=1) + + same_offset = wall_0.utcoffset() == wall_1.utcoffset() + same_dst = wall_0.dst() == wall_1.dst() + + return not (same_offset and same_dst) + + +def resolve_imaginary(dt): + """ + Given a datetime that may be imaginary, return an existing datetime. + + This function assumes that an imaginary datetime represents what the + wall time would be in a zone had the offset transition not occurred, so + it will always fall forward by the transition's change in offset. + + .. doctest:: + + >>> from dateutil import tz + >>> from datetime import datetime + >>> NYC = tz.gettz('America/New_York') + >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) + 2017-03-12 03:30:00-04:00 + + >>> KIR = tz.gettz('Pacific/Kiritimati') + >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) + 1995-01-02 12:30:00+14:00 + + As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, + existing datetime, so a round-trip to and from UTC is sufficient to get + an extant datetime, however, this generally "falls back" to an earlier time + rather than falling forward to the STD side (though no guarantees are made + about this behavior). + + :param dt: + A :class:`datetime.datetime` which may or may not exist. + + :return: + Returns an existing :class:`datetime.datetime`. If ``dt`` was not + imaginary, the datetime returned is guaranteed to be the same object + passed to the function. + + .. versionadded:: 2.7.0 + """ + if dt.tzinfo is not None and not datetime_exists(dt): + + curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() + old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() + + dt += curr_offset - old_offset + + return dt + + +def _datetime_to_timestamp(dt): + """ + Convert a :class:`datetime.datetime` object to an epoch timestamp in + seconds since January 1, 1970, ignoring the time zone. + """ + return (dt.replace(tzinfo=None) - EPOCH).total_seconds() + + +if sys.version_info >= (3, 6): + def _get_supported_offset(second_offset): + return second_offset +else: + def _get_supported_offset(second_offset): + # For python pre-3.6, round to full-minutes if that's not the case. + # Python's datetime doesn't accept sub-minute timezones. Check + # http://python.org/sf/1447945 or https://bugs.python.org/issue5288 + # for some information. + old_offset = second_offset + calculated_offset = 60 * ((second_offset + 30) // 60) + return calculated_offset + + +try: + # Python 3.7 feature + from contextlib import nullcontext as _nullcontext +except ImportError: + class _nullcontext(object): + """ + Class for wrapping contexts so that they are passed through in a + with statement. + """ + def __init__(self, context): + self.context = context + + def __enter__(self): + return self.context + + def __exit__(*args, **kwargs): + pass + +# vim:ts=4:sw=4:et diff --git a/tapdown/lib/python3.11/site-packages/dateutil/tz/win.py b/tapdown/lib/python3.11/site-packages/dateutil/tz/win.py new file mode 100644 index 0000000..cde07ba --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/tz/win.py @@ -0,0 +1,370 @@ +# -*- coding: utf-8 -*- +""" +This module provides an interface to the native time zone data on Windows, +including :py:class:`datetime.tzinfo` implementations. + +Attempting to import this module on a non-Windows platform will raise an +:py:obj:`ImportError`. +""" +# This code was originally contributed by Jeffrey Harris. +import datetime +import struct + +from six.moves import winreg +from six import text_type + +try: + import ctypes + from ctypes import wintypes +except ValueError: + # ValueError is raised on non-Windows systems for some horrible reason. + raise ImportError("Running tzwin on non-Windows system") + +from ._common import tzrangebase + +__all__ = ["tzwin", "tzwinlocal", "tzres"] + +ONEWEEK = datetime.timedelta(7) + +TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones" +TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones" +TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation" + + +def _settzkeyname(): + handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) + try: + winreg.OpenKey(handle, TZKEYNAMENT).Close() + TZKEYNAME = TZKEYNAMENT + except WindowsError: + TZKEYNAME = TZKEYNAME9X + handle.Close() + return TZKEYNAME + + +TZKEYNAME = _settzkeyname() + + +class tzres(object): + """ + Class for accessing ``tzres.dll``, which contains timezone name related + resources. + + .. versionadded:: 2.5.0 + """ + p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char + + def __init__(self, tzres_loc='tzres.dll'): + # Load the user32 DLL so we can load strings from tzres + user32 = ctypes.WinDLL('user32') + + # Specify the LoadStringW function + user32.LoadStringW.argtypes = (wintypes.HINSTANCE, + wintypes.UINT, + wintypes.LPWSTR, + ctypes.c_int) + + self.LoadStringW = user32.LoadStringW + self._tzres = ctypes.WinDLL(tzres_loc) + self.tzres_loc = tzres_loc + + def load_name(self, offset): + """ + Load a timezone name from a DLL offset (integer). + + >>> from dateutil.tzwin import tzres + >>> tzr = tzres() + >>> print(tzr.load_name(112)) + 'Eastern Standard Time' + + :param offset: + A positive integer value referring to a string from the tzres dll. + + .. note:: + + Offsets found in the registry are generally of the form + ``@tzres.dll,-114``. The offset in this case is 114, not -114. + + """ + resource = self.p_wchar() + lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR) + nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0) + return resource[:nchar] + + def name_from_string(self, tzname_str): + """ + Parse strings as returned from the Windows registry into the time zone + name as defined in the registry. + + >>> from dateutil.tzwin import tzres + >>> tzr = tzres() + >>> print(tzr.name_from_string('@tzres.dll,-251')) + 'Dateline Daylight Time' + >>> print(tzr.name_from_string('Eastern Standard Time')) + 'Eastern Standard Time' + + :param tzname_str: + A timezone name string as returned from a Windows registry key. + + :return: + Returns the localized timezone string from tzres.dll if the string + is of the form `@tzres.dll,-offset`, else returns the input string. + """ + if not tzname_str.startswith('@'): + return tzname_str + + name_splt = tzname_str.split(',-') + try: + offset = int(name_splt[1]) + except: + raise ValueError("Malformed timezone string.") + + return self.load_name(offset) + + +class tzwinbase(tzrangebase): + """tzinfo class based on win32's timezones available in the registry.""" + def __init__(self): + raise NotImplementedError('tzwinbase is an abstract base class') + + def __eq__(self, other): + # Compare on all relevant dimensions, including name. + if not isinstance(other, tzwinbase): + return NotImplemented + + return (self._std_offset == other._std_offset and + self._dst_offset == other._dst_offset and + self._stddayofweek == other._stddayofweek and + self._dstdayofweek == other._dstdayofweek and + self._stdweeknumber == other._stdweeknumber and + self._dstweeknumber == other._dstweeknumber and + self._stdhour == other._stdhour and + self._dsthour == other._dsthour and + self._stdminute == other._stdminute and + self._dstminute == other._dstminute and + self._std_abbr == other._std_abbr and + self._dst_abbr == other._dst_abbr) + + @staticmethod + def list(): + """Return a list of all time zones known to the system.""" + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZKEYNAME) as tzkey: + result = [winreg.EnumKey(tzkey, i) + for i in range(winreg.QueryInfoKey(tzkey)[0])] + return result + + def display(self): + """ + Return the display name of the time zone. + """ + return self._display + + def transitions(self, year): + """ + For a given year, get the DST on and off transition times, expressed + always on the standard time side. For zones with no transitions, this + function returns ``None``. + + :param year: + The year whose transitions you would like to query. + + :return: + Returns a :class:`tuple` of :class:`datetime.datetime` objects, + ``(dston, dstoff)`` for zones with an annual DST transition, or + ``None`` for fixed offset zones. + """ + + if not self.hasdst: + return None + + dston = picknthweekday(year, self._dstmonth, self._dstdayofweek, + self._dsthour, self._dstminute, + self._dstweeknumber) + + dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek, + self._stdhour, self._stdminute, + self._stdweeknumber) + + # Ambiguous dates default to the STD side + dstoff -= self._dst_base_offset + + return dston, dstoff + + def _get_hasdst(self): + return self._dstmonth != 0 + + @property + def _dst_base_offset(self): + return self._dst_base_offset_ + + +class tzwin(tzwinbase): + """ + Time zone object created from the zone info in the Windows registry + + These are similar to :py:class:`dateutil.tz.tzrange` objects in that + the time zone data is provided in the format of a single offset rule + for either 0 or 2 time zone transitions per year. + + :param: name + The name of a Windows time zone key, e.g. "Eastern Standard Time". + The full list of keys can be retrieved with :func:`tzwin.list`. + """ + + def __init__(self, name): + self._name = name + + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name) + with winreg.OpenKey(handle, tzkeyname) as tzkey: + keydict = valuestodict(tzkey) + + self._std_abbr = keydict["Std"] + self._dst_abbr = keydict["Dlt"] + + self._display = keydict["Display"] + + # See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm + tup = struct.unpack("=3l16h", keydict["TZI"]) + stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1 + dstoffset = stdoffset-tup[2] # + DaylightBias * -1 + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) + + # for the meaning see the win32 TIME_ZONE_INFORMATION structure docs + # http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx + (self._stdmonth, + self._stddayofweek, # Sunday = 0 + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[4:9] + + (self._dstmonth, + self._dstdayofweek, # Sunday = 0 + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[12:17] + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + + def __repr__(self): + return "tzwin(%s)" % repr(self._name) + + def __reduce__(self): + return (self.__class__, (self._name,)) + + +class tzwinlocal(tzwinbase): + """ + Class representing the local time zone information in the Windows registry + + While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time` + module) to retrieve time zone information, ``tzwinlocal`` retrieves the + rules directly from the Windows registry and creates an object like + :class:`dateutil.tz.tzwin`. + + Because Windows does not have an equivalent of :func:`time.tzset`, on + Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the + time zone settings *at the time that the process was started*, meaning + changes to the machine's time zone settings during the run of a program + on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`. + Because ``tzwinlocal`` reads the registry directly, it is unaffected by + this issue. + """ + def __init__(self): + with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: + with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey: + keydict = valuestodict(tzlocalkey) + + self._std_abbr = keydict["StandardName"] + self._dst_abbr = keydict["DaylightName"] + + try: + tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME, + sn=self._std_abbr) + with winreg.OpenKey(handle, tzkeyname) as tzkey: + _keydict = valuestodict(tzkey) + self._display = _keydict["Display"] + except OSError: + self._display = None + + stdoffset = -keydict["Bias"]-keydict["StandardBias"] + dstoffset = stdoffset-keydict["DaylightBias"] + + self._std_offset = datetime.timedelta(minutes=stdoffset) + self._dst_offset = datetime.timedelta(minutes=dstoffset) + + # For reasons unclear, in this particular key, the day of week has been + # moved to the END of the SYSTEMTIME structure. + tup = struct.unpack("=8h", keydict["StandardStart"]) + + (self._stdmonth, + self._stdweeknumber, # Last = 5 + self._stdhour, + self._stdminute) = tup[1:5] + + self._stddayofweek = tup[7] + + tup = struct.unpack("=8h", keydict["DaylightStart"]) + + (self._dstmonth, + self._dstweeknumber, # Last = 5 + self._dsthour, + self._dstminute) = tup[1:5] + + self._dstdayofweek = tup[7] + + self._dst_base_offset_ = self._dst_offset - self._std_offset + self.hasdst = self._get_hasdst() + + def __repr__(self): + return "tzwinlocal()" + + def __str__(self): + # str will return the standard name, not the daylight name. + return "tzwinlocal(%s)" % repr(self._std_abbr) + + def __reduce__(self): + return (self.__class__, ()) + + +def picknthweekday(year, month, dayofweek, hour, minute, whichweek): + """ dayofweek == 0 means Sunday, whichweek 5 means last instance """ + first = datetime.datetime(year, month, 1, hour, minute) + + # This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6), + # Because 7 % 7 = 0 + weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1) + wd = weekdayone + ((whichweek - 1) * ONEWEEK) + if (wd.month != month): + wd -= ONEWEEK + + return wd + + +def valuestodict(key): + """Convert a registry key's values to a dictionary.""" + dout = {} + size = winreg.QueryInfoKey(key)[1] + tz_res = None + + for i in range(size): + key_name, value, dtype = winreg.EnumValue(key, i) + if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN: + # If it's a DWORD (32-bit integer), it's stored as unsigned - convert + # that to a proper signed integer + if value & (1 << 31): + value = value - (1 << 32) + elif dtype == winreg.REG_SZ: + # If it's a reference to the tzres DLL, load the actual string + if value.startswith('@tzres'): + tz_res = tz_res or tzres() + value = tz_res.name_from_string(value) + + value = value.rstrip('\x00') # Remove trailing nulls + + dout[key_name] = value + + return dout diff --git a/tapdown/lib/python3.11/site-packages/dateutil/tzwin.py b/tapdown/lib/python3.11/site-packages/dateutil/tzwin.py new file mode 100644 index 0000000..cebc673 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/tzwin.py @@ -0,0 +1,2 @@ +# tzwin has moved to dateutil.tz.win +from .tz.win import * diff --git a/tapdown/lib/python3.11/site-packages/dateutil/utils.py b/tapdown/lib/python3.11/site-packages/dateutil/utils.py new file mode 100644 index 0000000..dd2d245 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/utils.py @@ -0,0 +1,71 @@ +# -*- coding: utf-8 -*- +""" +This module offers general convenience and utility functions for dealing with +datetimes. + +.. versionadded:: 2.7.0 +""" +from __future__ import unicode_literals + +from datetime import datetime, time + + +def today(tzinfo=None): + """ + Returns a :py:class:`datetime` representing the current day at midnight + + :param tzinfo: + The time zone to attach (also used to determine the current day). + + :return: + A :py:class:`datetime.datetime` object representing the current day + at midnight. + """ + + dt = datetime.now(tzinfo) + return datetime.combine(dt.date(), time(0, tzinfo=tzinfo)) + + +def default_tzinfo(dt, tzinfo): + """ + Sets the ``tzinfo`` parameter on naive datetimes only + + This is useful for example when you are provided a datetime that may have + either an implicit or explicit time zone, such as when parsing a time zone + string. + + .. doctest:: + + >>> from dateutil.tz import tzoffset + >>> from dateutil.parser import parse + >>> from dateutil.utils import default_tzinfo + >>> dflt_tz = tzoffset("EST", -18000) + >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz)) + 2014-01-01 12:30:00+00:00 + >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz)) + 2014-01-01 12:30:00-05:00 + + :param dt: + The datetime on which to replace the time zone + + :param tzinfo: + The :py:class:`datetime.tzinfo` subclass instance to assign to + ``dt`` if (and only if) it is naive. + + :return: + Returns an aware :py:class:`datetime.datetime`. + """ + if dt.tzinfo is not None: + return dt + else: + return dt.replace(tzinfo=tzinfo) + + +def within_delta(dt1, dt2, delta): + """ + Useful for comparing two datetimes that may have a negligible difference + to be considered equal. + """ + delta = abs(delta) + difference = dt1 - dt2 + return -delta <= difference <= delta diff --git a/tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/__init__.py b/tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/__init__.py new file mode 100644 index 0000000..34f11ad --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/__init__.py @@ -0,0 +1,167 @@ +# -*- coding: utf-8 -*- +import warnings +import json + +from tarfile import TarFile +from pkgutil import get_data +from io import BytesIO + +from dateutil.tz import tzfile as _tzfile + +__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"] + +ZONEFILENAME = "dateutil-zoneinfo.tar.gz" +METADATA_FN = 'METADATA' + + +class tzfile(_tzfile): + def __reduce__(self): + return (gettz, (self._filename,)) + + +def getzoneinfofile_stream(): + try: + return BytesIO(get_data(__name__, ZONEFILENAME)) + except IOError as e: # TODO switch to FileNotFoundError? + warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror)) + return None + + +class ZoneInfoFile(object): + def __init__(self, zonefile_stream=None): + if zonefile_stream is not None: + with TarFile.open(fileobj=zonefile_stream) as tf: + self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name) + for zf in tf.getmembers() + if zf.isfile() and zf.name != METADATA_FN} + # deal with links: They'll point to their parent object. Less + # waste of memory + links = {zl.name: self.zones[zl.linkname] + for zl in tf.getmembers() if + zl.islnk() or zl.issym()} + self.zones.update(links) + try: + metadata_json = tf.extractfile(tf.getmember(METADATA_FN)) + metadata_str = metadata_json.read().decode('UTF-8') + self.metadata = json.loads(metadata_str) + except KeyError: + # no metadata in tar file + self.metadata = None + else: + self.zones = {} + self.metadata = None + + def get(self, name, default=None): + """ + Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method + for retrieving zones from the zone dictionary. + + :param name: + The name of the zone to retrieve. (Generally IANA zone names) + + :param default: + The value to return in the event of a missing key. + + .. versionadded:: 2.6.0 + + """ + return self.zones.get(name, default) + + +# The current API has gettz as a module function, although in fact it taps into +# a stateful class. So as a workaround for now, without changing the API, we +# will create a new "global" class instance the first time a user requests a +# timezone. Ugly, but adheres to the api. +# +# TODO: Remove after deprecation period. +_CLASS_ZONE_INSTANCE = [] + + +def get_zonefile_instance(new_instance=False): + """ + This is a convenience function which provides a :class:`ZoneInfoFile` + instance using the data provided by the ``dateutil`` package. By default, it + caches a single instance of the ZoneInfoFile object and returns that. + + :param new_instance: + If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and + used as the cached instance for the next call. Otherwise, new instances + are created only as necessary. + + :return: + Returns a :class:`ZoneInfoFile` object. + + .. versionadded:: 2.6 + """ + if new_instance: + zif = None + else: + zif = getattr(get_zonefile_instance, '_cached_instance', None) + + if zif is None: + zif = ZoneInfoFile(getzoneinfofile_stream()) + + get_zonefile_instance._cached_instance = zif + + return zif + + +def gettz(name): + """ + This retrieves a time zone from the local zoneinfo tarball that is packaged + with dateutil. + + :param name: + An IANA-style time zone name, as found in the zoneinfo file. + + :return: + Returns a :class:`dateutil.tz.tzfile` time zone object. + + .. warning:: + It is generally inadvisable to use this function, and it is only + provided for API compatibility with earlier versions. This is *not* + equivalent to ``dateutil.tz.gettz()``, which selects an appropriate + time zone based on the inputs, favoring system zoneinfo. This is ONLY + for accessing the dateutil-specific zoneinfo (which may be out of + date compared to the system zoneinfo). + + .. deprecated:: 2.6 + If you need to use a specific zoneinfofile over the system zoneinfo, + instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call + :func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead. + + Use :func:`get_zonefile_instance` to retrieve an instance of the + dateutil-provided zoneinfo. + """ + warnings.warn("zoneinfo.gettz() will be removed in future versions, " + "to use the dateutil-provided zoneinfo files, instantiate a " + "ZoneInfoFile object and use ZoneInfoFile.zones.get() " + "instead. See the documentation for details.", + DeprecationWarning) + + if len(_CLASS_ZONE_INSTANCE) == 0: + _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) + return _CLASS_ZONE_INSTANCE[0].zones.get(name) + + +def gettz_db_metadata(): + """ Get the zonefile metadata + + See `zonefile_metadata`_ + + :returns: + A dictionary with the database metadata + + .. deprecated:: 2.6 + See deprecation warning in :func:`zoneinfo.gettz`. To get metadata, + query the attribute ``zoneinfo.ZoneInfoFile.metadata``. + """ + warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future " + "versions, to use the dateutil-provided zoneinfo files, " + "ZoneInfoFile object and query the 'metadata' attribute " + "instead. See the documentation for details.", + DeprecationWarning) + + if len(_CLASS_ZONE_INSTANCE) == 0: + _CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream())) + return _CLASS_ZONE_INSTANCE[0].metadata diff --git a/tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz b/tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/dateutil-zoneinfo.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..1461f8c862df8c3eae75e0ea376788b5b2602269 GIT binary patch literal 156400 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

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
\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/tapdown/lib/python3.11/site-packages/eventlet/green/httplib.py b/tapdown/lib/python3.11/site-packages/eventlet/green/httplib.py new file mode 100644 index 0000000..f67dbfe --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/os.py b/tapdown/lib/python3.11/site-packages/eventlet/green/os.py new file mode 100644 index 0000000..5942f36 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/profile.py b/tapdown/lib/python3.11/site-packages/eventlet/green/profile.py new file mode 100644 index 0000000..a03b507 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/select.py b/tapdown/lib/python3.11/site-packages/eventlet/green/select.py new file mode 100644 index 0000000..a87d10d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/selectors.py b/tapdown/lib/python3.11/site-packages/eventlet/green/selectors.py new file mode 100644 index 0000000..81fc862 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/socket.py b/tapdown/lib/python3.11/site-packages/eventlet/green/socket.py new file mode 100644 index 0000000..6a39caf --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/ssl.py b/tapdown/lib/python3.11/site-packages/eventlet/green/ssl.py new file mode 100644 index 0000000..7ceb3c7 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/subprocess.py b/tapdown/lib/python3.11/site-packages/eventlet/green/subprocess.py new file mode 100644 index 0000000..4509208 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/thread.py b/tapdown/lib/python3.11/site-packages/eventlet/green/thread.py new file mode 100644 index 0000000..224cd1c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/threading.py b/tapdown/lib/python3.11/site-packages/eventlet/green/threading.py new file mode 100644 index 0000000..ae01a5b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/time.py b/tapdown/lib/python3.11/site-packages/eventlet/green/time.py new file mode 100644 index 0000000..0fbe30e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/urllib/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/green/urllib/__init__.py new file mode 100644 index 0000000..44335dd --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/urllib/error.py b/tapdown/lib/python3.11/site-packages/eventlet/green/urllib/error.py new file mode 100644 index 0000000..6913813 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/urllib/parse.py b/tapdown/lib/python3.11/site-packages/eventlet/green/urllib/parse.py new file mode 100644 index 0000000..f3a8924 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/urllib/request.py b/tapdown/lib/python3.11/site-packages/eventlet/green/urllib/request.py new file mode 100644 index 0000000..43c198e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/urllib/response.py b/tapdown/lib/python3.11/site-packages/eventlet/green/urllib/response.py new file mode 100644 index 0000000..f9aaba5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/urllib2.py b/tapdown/lib/python3.11/site-packages/eventlet/green/urllib2.py new file mode 100644 index 0000000..c53ecbb --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/zmq.py b/tapdown/lib/python3.11/site-packages/eventlet/green/zmq.py new file mode 100644 index 0000000..865ee13 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/greenio/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/greenio/__init__.py new file mode 100644 index 0000000..513c4a5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/greenio/base.py b/tapdown/lib/python3.11/site-packages/eventlet/greenio/base.py new file mode 100644 index 0000000..3bb7d02 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/greenio/py3.py b/tapdown/lib/python3.11/site-packages/eventlet/greenio/py3.py new file mode 100644 index 0000000..d3811df --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/greenpool.py b/tapdown/lib/python3.11/site-packages/eventlet/greenpool.py new file mode 100644 index 0000000..f907e38 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/greenthread.py b/tapdown/lib/python3.11/site-packages/eventlet/greenthread.py new file mode 100644 index 0000000..d1be005 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/hubs/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/hubs/__init__.py new file mode 100644 index 0000000..b1a3e80 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/hubs/asyncio.py b/tapdown/lib/python3.11/site-packages/eventlet/hubs/asyncio.py new file mode 100644 index 0000000..2b9b7e5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/hubs/epolls.py b/tapdown/lib/python3.11/site-packages/eventlet/hubs/epolls.py new file mode 100644 index 0000000..770c18d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/hubs/hub.py b/tapdown/lib/python3.11/site-packages/eventlet/hubs/hub.py new file mode 100644 index 0000000..abeee6c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/hubs/kqueue.py b/tapdown/lib/python3.11/site-packages/eventlet/hubs/kqueue.py new file mode 100644 index 0000000..9502576 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/hubs/poll.py b/tapdown/lib/python3.11/site-packages/eventlet/hubs/poll.py new file mode 100644 index 0000000..0984214 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/hubs/pyevent.py b/tapdown/lib/python3.11/site-packages/eventlet/hubs/pyevent.py new file mode 100644 index 0000000..0802243 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/hubs/selects.py b/tapdown/lib/python3.11/site-packages/eventlet/hubs/selects.py new file mode 100644 index 0000000..b6cf129 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/hubs/timer.py b/tapdown/lib/python3.11/site-packages/eventlet/hubs/timer.py new file mode 100644 index 0000000..2e3fd95 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/lock.py b/tapdown/lib/python3.11/site-packages/eventlet/lock.py new file mode 100644 index 0000000..4b21e0b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/patcher.py b/tapdown/lib/python3.11/site-packages/eventlet/patcher.py new file mode 100644 index 0000000..12d8069 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/pools.py b/tapdown/lib/python3.11/site-packages/eventlet/pools.py new file mode 100644 index 0000000..a65f174 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/queue.py b/tapdown/lib/python3.11/site-packages/eventlet/queue.py new file mode 100644 index 0000000..d3bd4dc --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/semaphore.py b/tapdown/lib/python3.11/site-packages/eventlet/semaphore.py new file mode 100644 index 0000000..218d01a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/support/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/support/__init__.py new file mode 100644 index 0000000..b1c1607 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/support/greendns.py b/tapdown/lib/python3.11/site-packages/eventlet/support/greendns.py new file mode 100644 index 0000000..365664f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/support/greenlets.py b/tapdown/lib/python3.11/site-packages/eventlet/support/greenlets.py new file mode 100644 index 0000000..b939328 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/support/psycopg2_patcher.py b/tapdown/lib/python3.11/site-packages/eventlet/support/psycopg2_patcher.py new file mode 100644 index 0000000..2f4034a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/support/pylib.py b/tapdown/lib/python3.11/site-packages/eventlet/support/pylib.py new file mode 100644 index 0000000..fdb0682 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/support/stacklesspypys.py b/tapdown/lib/python3.11/site-packages/eventlet/support/stacklesspypys.py new file mode 100644 index 0000000..fe3638a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/support/stacklesss.py b/tapdown/lib/python3.11/site-packages/eventlet/support/stacklesss.py new file mode 100644 index 0000000..9b3951e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/timeout.py b/tapdown/lib/python3.11/site-packages/eventlet/timeout.py new file mode 100644 index 0000000..4ab893e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/tpool.py b/tapdown/lib/python3.11/site-packages/eventlet/tpool.py new file mode 100644 index 0000000..1a3f412 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/websocket.py b/tapdown/lib/python3.11/site-packages/eventlet/websocket.py new file mode 100644 index 0000000..3d50f70 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/wsgi.py b/tapdown/lib/python3.11/site-packages/eventlet/wsgi.py new file mode 100644 index 0000000..b6b4d0c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/README.rst b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/README.rst new file mode 100644 index 0000000..b094781 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/README.rst b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/README.rst new file mode 100644 index 0000000..0317d50 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore.thrift b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore.thrift new file mode 100644 index 0000000..0787ca8 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/__init__.py new file mode 100644 index 0000000..adefd8e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/__init__.py @@ -0,0 +1 @@ +__all__ = ['ttypes', 'constants'] diff --git a/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/constants.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/constants.py new file mode 100644 index 0000000..3e04f77 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/ttypes.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/_thrift/zipkinCore/ttypes.py new file mode 100644 index 0000000..418911f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/api.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/api.py new file mode 100644 index 0000000..8edde5c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/client.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/client.py new file mode 100644 index 0000000..faff244 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/example/ex1.png b/tapdown/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$ zfgRiAv<4*;H!6vk51OD_Tpe?+b$#`jS*_*!^{!5NbG?1HG&vwn*B}q04qQHp+Cn8x ze3ffX%Wn9K6ZKjLF3oB$XjUw{lxFCf=vDF%h|r-+pgFqlnP)S5lBIz>y0d7H;>2g= zCy!M}uwLDs1+^$#ggaE$3AD-ERw&nq(+l`pJXv*^9q3bySpyeS9;_R^2bXQUhGlPF z9?7T3QvHUWCC1+(^F|&S_s0moV+inj`N5#G3BVekTu!?*jw^}NPGuB)Ar4fVqQYCk zZR4T7GaBr2!Vn zs-RSw6|ucRz_Tfc+%ev(L>s!=r3S)Sm^HGL+;G*+K5RKE^6#aJ=T|&9PHVZC`@Ip5 znro}sn2j=+xCzUJyqbR07dAVxWP$LGj(auYQ+u6T<{R^r?xoZBdOC(Y3z}XgT~aYX zOUT;_Z1oQ-UxE9nGSPI##N4c!kx({ias*Bx_4xFB=u@0Uv0)mn- zd?SayXm3h}&gK5}m?tN4pFsXS+AWel{<+!h>7+;OzBpGNs%OKO6%jco(=ZZdEatA7^-$=^Pu+GcM?sU zg8$zF%uASx!E3n-RS!VhYQqodKB|Yym`9M03T%_w(nO$L64D51@mqTRvFKMDF|eo3 zbt};+=nN6f8V1j2U<7?gj{?U6C6}4{v`+&@+gA*YLm!)|cIBMv?HOKeO?#C3s^n;A zfv@I>uh9Yj=p{OPGg^PIu8jar9C@3~P-s;i5GUuDmB^8snbCF2xMSgHH(hBfH@4>I(UQT&$Ia z)IO9}gC_6{<$NZSmSEr6^MN zF+AU}f;d$BEvUz{OxS>!sO5Y?sNPkYU`|Ey-RLGX@raB73DwvpJYB;NrCK6+o-?`@ zS2bxf92_SzJJQ7qe_NN7^sLk+mWfR*!vT#gfmH0~syOtn$?60ozjop+=Fp#cXbzJ0 za3#UXaVTZZ2 z=r#_ei7)2uBuoGEXaaHm&%U0k=ALhNmjBy(c;BJf75m@pa|=3W;$-OHbfIk~!7u*JAuHgVkG5n9G#wmsQA7fEbkUYo>k>5H zi>7m64!k*)C=s&I<4E2{#!BN&=R6(o1M0o0b1X$vbjZYA*MpOn3R8KJRD)dxIMjwb z>Z($%%A7SFRAOSnfVyz|~-5*yS+1s7RQhTkK>x!WVx_HaSI@Vn8!nUpHx~seC#{ zF?aK&sgYw}(Y7rx-Gp_X0d6PBT*XK>Dah(FT>(mLEDc)nJ@XlXl?BH&6O5X~4C}vU zo^DjPWZW|fSXm}XIW!-@==|X`W6Xs?Jsud+7+r5ZHRu|Q#Ctbtaz4^5Z*?|cS>01$4moP9Wf%?GPCDFh?sXBBe%C;vPUPAY0d-xelmkm17P}vipP`IW z=Gt?yb0(v~Fp!ERS%F~q@|E_3r#~Zr&KF#W-{SdTOPkTml_5;jWw|(dcu_^}D28%q zxk>GHGZf4LU++t)wBd$W!kfd(0C%GFeba}75o!gYR1Ddy z_eq4ONX++cFtzdmYJ!ajICUBP6usx1O1*yl5tk0c=0b_q-~yNIuK`=!(KHD#>ss8+d(A#W%z*=FrT#S z_hEjcTu5iOmV#808XnRYGnlZ^I|X-pgFo{O3)ghA)YU z_p(dr+Z|ott2%}`^W@o9xiwQq8tF>qc#2Dx3z*=Dnu;=yA*I~9l_Okc35!X2sP2j% zjG|BKU1jP){5c|zBYWPpiQBgH1%GR8<6TZ{$X z@=QVJCR5jJ{l}?>wB14FCg0`;B5a+e=Aq>&pm(w=Traaxp}@=XKid2Du%^y@T^y${ zGezhgw^homjPCMc+XXl_k(VA=;=Q>EAsw|clzC^ zvjTstD+3h=7C?vJPj!%gRhAV3EVIv5Y^`MsdW}KooJQLWI{5^3_goQVgHKNgl>T(c z+5#9AUOond~`$Se~HP-sQ!l&5e<%2 z9SE>3*a+|`)1%OO5w-seK??){jxzyWj#pc$*|`PyANtS!2Yq%_-xW3rj@830pg=-) zZ@Xkn(H#BC%3lZS&Na4oOFL#jH8a5cn|$#rjc+o*6!Ffd`0>Y}p0Ozk1FGTGJ3T;k z!k{-I48L}I4Y0R)`o3-xejeIKiY%c1ebri?=BEMdk8t{i{chqqdyPq`nV2N{ zf#uVyD4xy5(hjaMdr@4m+mykvtz@-e19fig8g_cI0W)L++oMmA+3SsfquRXcR{<<8 z(n8}?b)Mn(_!HqqCG!UBHt@iW6oCKGxH11>Jv3f411>_m*zaE(VIVDt*(hAnx-%*S zPKbN+X-3$)g2}JH4hln`&TQ8KAO7(oZt7=W_2CNG4h6thJ()H`GV*H9C3wE&{23ed zyGi;MgUesB{DWq;Z@oR4H(Z9$_}&nzIU2~!EkDFxa9=!fHHGobTIDl?t-Puu^>3T?-t(ctW+-N|;(8}A193l_ zfu_mpN2X~y(;Yr_EOQG>q3m+_nNhv^>NiYYS@QG4OG!3KY0uR_e7 z{ZlpK@b!UN1En7pW63#O`Qq+u?rR+B2lx4Xmd5nV=g`uEPpgmeEvtTNSA1~zB)!tj z6sf(9*taV;kVtbo_WD-0k$xbGRvqRi9DA<@T86sWWtDnz7<UpQP#J!hj<>`24p8J6c^%*=GvuP0Q1nDiC+LoTd88+fCcGODXh%_Wb9_TDQ(qCu z58yAQ_pO>c?KZJqp{^_|RzY{+L-7t<{U^-}v|(QYX6i@;bjJN+i%P-|!r(lJm%2(L z)vng9lDm8W7(_*sYX#CGska;>M%Usqn~@sj^OWGjC36`6fs&HUpzC)(l-IuZ;|w7# zd~+tdjn+wgsvLI@@l>|>Q;@KAl?B7`taZ0C(V0Bm-zG*N{?BdTEs{E+DPN4<}ah2BbR(-D9e&N=L_3PDa8R%r)n~BacZ_j_sQz zNAmrIqU7lj3pzpSC3TN7FmeFDF4ZAI>a-9a*Lv-PqrA3Vu}9fVyTbtm>Yz=Zt5~kJ z-!S3f{PClcn)%Y2GR)|%AUeq@y>pD_Ka!oqZSWaYH`HtL37g8on@+YBrSy6R zsKuk?1v1s$GCUgX4(k4_OLSofN?9T%tnxN;tNCkFmy=kasAjJX#bz{vigdd07|V&o z4_z7P7vJX3IdS!psU@AE@b8XS=@g_Q{Az^C>tQHp8Vpb0W3f7Ty_7t9bGuV{YTU5C z-ZSTzTdpdzD##ia)2!)uyaeMmDZfw6?ip;a*MGzZ8>eUSgy$%@ZBJN*5U4@s0Jbc*!fOd>y$r?(-NCG8)nSS*A8oj zcBR;Qc*~e?q0tCrySr0vTR1Zu_|~3>yGl>yP*E9Op0e>$NIZL@Cz60_mX)_$Pwgs8 zp;Tk2e@QDi8ty!kQo>=*KTljJfcZmSf;(;A2Bd;?a=oYO#Mt7&iO-Rom34A9)y1giApVaHFDUv>l)Fx^g z-ClH|wWZqW@9E8gGnw*7Su*J`w5qm!iQ{n(Iik-n8Bl_Id@JN2YSWjhb11w9Akalx zBKPV4^py4=I%=QdU)(&2=xn~RL6U3kb#hIUBOQ#qXofC*anHh|oSt|vE|7zhDq4HG zI8FD>;!tOCn$T3V3B8Qh9XqUj*U3Sz=u}Q0hg3&EtT;6PQuj(LP}!_ttG)(B6oG$duGs`TJ+eEC6Jc}ein!MKr zAUU*}i-Uk9Y>LEp`Xsv?a+RYI{M$&e_leb+;x_JZ>duE z5L6G^)bnQZ!l1R8>mCC$Uf6jiK@bj0;;=g%i4{`)5dQIZ&@zlPOMH(wXOU7IfNF1# z$M+VBcT|Z^F;gFhEw$z6oo|q9-;XOX81D;; z?L1)^mpFqk{T!kAw7Jg?>eTKbCa-87THvnf?D|kk z?iqD!Uz+VSrkfSE#bWYGz=HL7PrF&2{Uj?;C_A=qqHPdylCCJfhf{qflmdq{cw?{u z-~W@-v6>kXNJYnri%74Wy@; zDj}GcrJVOJ+d>z$%y3#ppmryv-u1W54nuxqt6qcw^7TKvRV95rDL_(RLP>A)lQwFfAHscfXBsHGfNgJ9zU`x3LqEQ z=OA`bkf$@D6(wAfb@NKfq!F|_g8FCG?SAh4oV{yfey~7hJ(t-@DF{qW(9YUgo$=AD zu4UNV%ZcDTTI%xg{`^(qQ3UA{QE?{mkR*bHOQsd0rf@LWti?q$9oMDHVZz9U@(oNw|7g61lwy>#db3owtbKf)mQ zF*|cIk-*Np7hZ*URk9($xGRakvsz25r+@r)V~o&VEyy6Hzj~Q3p6?FEtdwoUoieGl zjITR@;bn;2;*S%#ncm|K)a+wb`5T9BL`ni{hg;e%c5Y%JZC4HzXX<@BX;m)(tnc#5 zoSM-NW9CBBFRfY$GbBFF$<24wa#e*wTsy<)(O4X}O?XzZ1WDorN0-%W-VG_TwN~_} zUz`N#IzQ<~MXD5012B`aB2crasRnecYTThrT>lV==!oL8nOViJ$1|vGc2>#bE25~uvDe>61`fnHErz& z4v!~F^pi#HjA@a2pNNZ9g&i&_X={&thZ;e_{44X?qQSmSy<~o%?S`2U3?fGDH%fx6 znJ=F`F2N#3g!^6-1h2PPGa?iCyMX1zxxC2YW7lR^e3*cT^Xhqp@76d)V@$@xYp&nn z>6F)5;7Z>qb0i(0Mb;A-Iw=7mOF2r0B2cBj^hQRn8QLZVwDD4xv%mcp-D4U?8W@*; zNixx%uWixui~suX{-*xktdYBxVsdBjTuZB%>G#3?=nX)n@6r3tP1f%tHhS*vNajJ$ zI4)2cmKlJVP2m#^EM_r~ z=n5ZRL)SWO4NppvMQN@7@t274K>})>SKDH@A)N_7SsqR!ho-5&tdG}ZmO|oQ$jE=J z>CR(Vu0OQ9_FGHV6{w&N9WtVEK-qzo$=liKGK&1ZlzWqY4?AwBy^-sNIUJnISn83b z2=15Sy1`D<4w~ab!xtU|5tarc+S?+fVLio!Ft>)9t>TB_*a_=Sj<&=9nmr1qrTA94 z0jgF`ChDj(e3iI;h^>Z&($d(2OZ9H-%!{vdi!XECY+5)52_+hCHbq?3Rqit5eA3NR zUt+O7MPnGu&oGDzhOv0S2H}^k4$2lGh8N!U2@qz!jX38v?0A#$eQ2tv)3*xP)!^o_ zxwn?$viPoUH|IVzJMD}ey#$^QB=@a7w7Wr!bZX)c?t~j}Q;5N>GeyHoy2@kw7-lX> z!lhK7Gq83Z%-6S%N@=QyZ|eJ0JaC1whbKhihbLsBJt%?Lp%a<F@4fUb_uvQIm+?L{g zKLpXbYKA`U`Mf-1E;vX?3#%gix}(_N%?W~)Eblz~-Tn?CfhM)74p?$Isjz{4Tt#~; zuSiUkMJ26Nbdf%S0Qsh&Z}}!RVP{dP-39&xzarAsJSY81TOn`Co=#tHpS$qdHl_DM7H9*st{>UzLHJQxb!~Z!w6Qq>k3}cFnvYvv0 z87{sprZ9*XdwTea)a#VVb&Gh;*~K%(jfs0P`pP4$02a4BR`^59pIx(9SLVX&dMd6b zjVj@_+fuI4VwbKLs`Dc+7+0h^rMH>+QfC!ny86cef1bX)U6XAebT)N(^HWB@y0p9nI#pG#$^_R&}vvvbOI63(XPV}%F5ohwLE*NWDD8i z43xlrq$tVQnwyTxrn8^9Mcr$S7x#=U7}GIiixFs*XHfk$*Cs>;#rZ0NnR7$wyp1!_ zeZEHBs8Bh7&wd9==;jSWhxs?B&$Yy5@3uGuKbJnaP(`qVb1cA?)&YYuE1G|j{z_ze z+6U~<=_%}7oK)=G+xzW6JZs9-DPpCbz|+I`wRXWoWS{`E_{^|;c3B~YMN5+yoDSqRYw!HE9GjXfmKAd!Ncv6%7+)py zX5KT4rtWdEI~WbpeWwE1u?q<%Bm%4S?{}6M-#t@%B~Au9T9~0 zUzCE931+5DD#kf)vq2^B-S1f=58!l?U3f4YrTmJSA%Q#F57?x4*-D%Yj&-;S?#g-@JajB<;E}69_Lz= zFq6+rgsc_r&sM>~DZKf2&>SDm`;8XY;g1p7WQ*f)bWC=k+PkH}>=HE-9VmcD#mw#4 zC@5uK(jWjT$g14Eb4XI}?ve=Or!**0_>b(6c24B=hc>!nufHF#XwP~7^ zVg;`vPqLE=YWdle;N7KC7g8#51Cj%>!_T5Bhd;`}c+1-gtIn}sm<9;-<9R$#YY0W^ zfDqsIXeMl2a3p6_SYnH^jaZ~F?wwcE6Qc{A<`^iApd3i77Ac*LIMaM@^=LbGCV;el zp@|&@;4^IL1pK=arDO9Hr(PK%4D=}g)sr-FMl40-QtZ1^nvvOkBqA`#QhKQ>qU;hb ztb%W9P^OPlL<^W{)jYxufA?B6(YSIpLcLONm{FzMylR;-^U!Z}&|q1!{_Pd@fn07o zr_!wq*H{d~n@SMqEwAdMmfq z70@c;=1|=VRuz1|7dY@e>jz#V7$TP()#wIILzQTyrbeS1E`{lMRX||~#1j%Rj}SN4 zR%;e4s#7`wy`|6LTVjm~!3dylM3JxVA5ED)81#aBn_ZEe#4t@u4aG2kD{80d&8Npp zYg<;!SJG0`m4M`KzBcttT=Hb)Djx`-<$15Rtt@-8{xn|c?^TIYA*GQO%B5;I{w^)c^^S2#y)AkY+IgZ=5~I8y!jimmbU& zGSe%)FFDR|PexJSgZlpXbUZ!DBdd>dnrz_IkM+*xT3v>}Dn|I*i;85Nw6Xkj27F)X zfs27E$%3GD*)1=sL7BwZ=}+0`V|PZ`{E(f=4qHo0!kKxN`da$~KG4W{TUm3@x5BP+ z>;uX&d%qjKSm|fkBB3UF1`XW>$u@`$!uaRz%pUKyfZ1Hq#h{NDcfK9HpST6oR(Tn+ z{a}+ZMc#F0KGZ+C`SMxFV(*68u^sqe9~Lwx5XbTqo_1^-r=_m}h?zw;b9Gc+ttP(c z&|pX5b%KB4_LG|Pt&@NJSlmfkPS(uWWSF<&dNLW&baP%Upc=Ia<7mS)D-~;VQ1i6` zwquh@($WImC@_szE`7xlK2IOUnHcQpy8Ca~G;{m+h6g`*{q7#a)z@+8uAMjMMvU#Q zyPWT#MFbk zeGdpc>uJvVoe@Sm4i{_!X-?VeG{?wj5RTZm?QlO;Nnze^OKF{DC6TlOMVr*SHeTPt zBfMSP+`Lr(kH2)3W;Yjn#wL7a^4V0zR;S(mP9cGgZ5aoGX*w9ExN#bN_trN8-qWrJ zeH)D~HwN>KBr|qMd#90v&Oro3;;<)Txd#?CaWwqoU>Lw$wOFU_sftKsKU1Mr=exvP zYSYodsMRHwWs2}lDP|bmNg+LHS-n340AUB3m!EiMm#MQzR>4A@Dl7+-j;<31S-CTU zYCJ^Hq7mk>-UUU5aMj&rb0B&f4#+{ihMEDoXRvmZqFWp)$PW8-+D{q*sRXSl?rsD6 zrx{?6;sD26psX$LOE2sG{jUrBA^$Dv)*G_9jI>va1^!NUTLp~oao#}dFhB2fCD>DO z)cj6@DLhqxAs8f8DD@83=pUxg7cm`Z2jXP$WrR8Wbf{*@iGYGpp4BN_dic{v=E>J$bZs;GP=hJyVbWLl5%SKE+P$Bad zgPG0Y&??(>-8aq?U-)I$e4aVhcV{Vu;H62%T+m<^mKC+!5KI|oJp)k;^rO+pz$qx9 zIYxt!thnRX&ifA(wz}&L#ni8kl#}xu@;>Ih&N=e#D4B{g1V>SMB+`Z-#i}K;0P6lB z(n&1yBc(lnT@$PwObYb6CJ1Dzhp*UF^NB3b9SXhsjS$Y1huDOAB{Q%-Y6twa50gM2 zD9FvQvvaMOSmos`R))`F-wBgyX+%bxyZB4HX`WkeaRhUvMqZeTAM;&G4!i@O$IP9E zOn;yBItc-GgFhSn)|_QZdVuED9LWI;f6R^|&ZgYk_>vb-MGT+I+FIm~Rb5$1bmN58 zy>zxjo`I$>TwxK~Rk;=XSMfacY#?m)PAxgnV77LV@R~Z-Rr}8A8(RI-T|VCNfWKH3 zhFz)yZ=EZ@k=iEB?Bo=rhXoG83ug}*SdzLBNH! zd9bm|6dLW2PVPMmxF7nyJL3CdX@n~(GHke{WAb3C(;4x6XY(9}8e|&j7xGqyMj`WB z80cSWmpZk_8(*V4r8NknDflJEm1#t8=H!B6?EDcKy56C)$Mj=vFfLv4@&4tRo~5*!$V2GrAuY{HNc!4newcI}ND_Jzwcz zIon*?ix4{FFIj&He8C3glG;ai)rNOr{ftY;#BFiN-62TEQESdM`d?JfNVY-dZ1~ng&yW@Oe-X zR9(n|Rv)LXs5oNv3v!^fdz1C7%6T7xQjkcrP)HUg9}dbIu@Ny1(nM#lV2JFx=FgZM z8gQKuV1WnW>}NYScAI{d=e4$&__)`7v&W4~#zn#CmQ_pP8UN`0n&$*Y+nU?PA#ari z?tLl|wmF5&=gu#^AR=MO9(0YSb_SB1PIaJ>fvW?04G*J3(-91|;MiOh2=4%xD>5;= z`e<;UCzvX?g;55?JylRXd z#^z%IW=djLA10hqaA4-?fmu)5!*(JDq_JjKgIb`koJN( zi=`Ye6DpP1udyEnv`TxuzLcTXX5io0_CpLfWqW_za9(s@Xy>nKt_+^C$t0T7W7i>k9Oi zW`(BYKkA5PH_h4zOC*Eq%}%yB8{U>!7;AVlPTJv?Mq23|+|y!Tjl5x+2`plK|J7R7N}y z%W0jw>&fDvFd?hG0z>kqjsGdn5H;xZ2g4Q5356ibIIvp&yq2@Co$?G}{1hPdNSCVb z&pi%dFs(2>m>PZw4L>Slf%00j!ha0~1hZ_=pqB9A=0~R#D35a<^WlSdL$G7_nm|g~$6c#{$TR%aNPJvNqHvzu6jrBWDz`af zdzX~%Er7ss$xG`G!sX|Y62O}Z(OC^RcCh=>y;l3`D)NwN!d8IL0Ucpb=W z1{-}Fmptz3#yd@a9p%XW2Bh7nBdS|6B!cn3d!Z_e6p8X=(nmt85_<5%lI@X`32p9W zk+EhI{LoY|j?VaIGlMpc$Sw|M5VdWo3#d7#jW5@;hw7tBi&}SlTL&U3!kwJR#C@*e zVL~BAqHvo4Wa=$2d11?A11TzOfjAyVlj;_XZ=d%j6foG>K&13f{DzjiI765^LPi+% zr`($~waweN*08d)o;vJH0`)6|Q)`s!nb-`bAS{6!57l6cTuc=fuuU}RJWjkA1o=()kEu15%l<^qTo z3_CJ6mpfjh+{D%3>S(VUABIH7 zXm8U(hOk)hxzi!zn3yJ z@jWbMDpJh9(_a})Q8LU4tL9z}@e3t2>mWfxRv=!rT}x}8&jy8LdMGRS(vKV6otI#BJ!V(sfPNwc0r>s z)ESGvMP%?+eXEi@{jBgKb+gSdGSG1}6cIgB2X%?u9Y=Zs07I!m7WTGnwKRGs0&;_a z=%|d+NYaN(ajyWU`>KhJ1?{Ek3()@Mhbboe#rb0C;|W$f3?)W;ur5OhtZBh|%i$GN z>a#rbJUx8QiC&<6O3SxH)-q5hyvz7!>U0aCF42z>w=K*a+gGas1vujljA?W4#XJxT zy`P)nLvX-*VQk<|>0~7LJX759*q6sjN)h#J2jY;G&B*qRhGjgLfHs4vSA}1!JLK@i zTbul>$>sRj8Zqep+=~$HjmBxF>C7PE>KEp@tq!>)*A* ziH6zmZFSg})Ce-6_0rszTL{Amo%MZJB0`zEMwIF3rz0s^>*?=PZUcxkHNJIJwM26! z&$b;irr7G1=fpoM9jgH(##^If<%Hd>2}02hYF=1KYZHH2Syr7(Cl54y(>T89gyQW} zWPq*rnnbr`M?{}eJQs3%g{jk%206}oVf9CJCl)Q>U z?#SxnZ}mD$2Vz&BW|%!`A(@6LOM5ZP3+cll5+_QhOak5Nh^eQ%Q{Ibk;mxKRRu=5g z{H(4;&@b?q`*z3|j|(-V$kRqkWIN=|d76ttr9?a8|3!4(X)b-|@o$Icrw!pbz{}Ow zL#yNcO1v{t0BOEFNZ7;*$#(PvGK+QTKUcD!B`$3CFJ#RHf4r}^;u`gLu31muk8YlR zVBrYg5|iD?HbSF8!Tc5+9hcP2aM!pVG_|DB zLIvM963)F6KZG}1ZJ2Oal*V6J33>M4TdL=sQ^yWIeC~Uft?DPz01XFs(k@qY)oay zG)9UX+i;R~Oqj_Pb^V&5D#iZJabw?=Ae_Vy1cTX(SzK}xLp$CjdmmDL95T=*@4JKk znmAkC_)=8?Adl>Chd@={_)cX_6ZuJY%Fl|Wtn4xwc z@s)ADRZhU>1_PF$>`(lMG*{^z09Yx{cyZO^5F0}fBu5V$`tBmHv9Su)>qbM_>Zd?# z!%#mX)7Mt2!O-Nuxw%CN-ON(paY|9rl@Y8rl=VO|dLY)gaBFXQgwKR)=ZKZijXxdR z7sZ<1?^*@9(3=4594rM*8e(KTwPtr`PpBh_KxDzc_5`3Fr6`^inV0m3iFlbIeJkNU zWJ@O{&pI_WHsS8`j6}_B3hv45m)Iw>JzJQS-k|@G^>EIB;pu9+pb-Z^-Y*BI;;+A+ ziUAP*O1Tn9G=$t6%jB+=l2deBH^$wCQInxV}ObmT@*c!*f zyt~vC8M*JL`joSvcg%&^=tv8~#oSKhE+7d~fSy zca@>Nv*wX_VbZwp0q`u%6chQA5Id2`Wd|9_*Ypud?- zyt!!f-*d+6n-BNq!>wEYHzsI(8Qz>?!!o?t4FA(MgI=$ixoLRm$m9Bf_s*LStS^9( z(bwO+#w6T)4M})&>CH;~v^y@p*%NP0$4^_~jZt`G6y6wx^%Zz?6y6+#H%H;kQ2;CO fKPx~wgfLlHHmWZ1vX3%+{lf>3eM0%`xi9}0rZ`Dz literal 0 HcmV?d00001 diff --git a/tapdown/lib/python3.11/site-packages/eventlet/zipkin/example/ex3.png b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/greenthread.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/greenthread.py new file mode 100644 index 0000000..37e12d6 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/http.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/http.py new file mode 100644 index 0000000..f981a17 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/log.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/log.py new file mode 100644 index 0000000..b7f9d32 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/patcher.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/patcher.py new file mode 100644 index 0000000..8e7d8ad --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/zipkin/wsgi.py b/tapdown/lib/python3.11/site-packages/eventlet/zipkin/wsgi.py new file mode 100644 index 0000000..402d142 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/METADATA new file mode 100644 index 0000000..46028fb --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/METADATA @@ -0,0 +1,91 @@ +Metadata-Version: 2.4 +Name: Flask +Version: 3.1.2 +Summary: A simple framework for building complex web applications. +Maintainer-email: Pallets +Requires-Python: >=3.9 +Description-Content-Type: text/markdown +License-Expression: BSD-3-Clause +Classifier: Development Status :: 5 - Production/Stable +Classifier: Environment :: Web Environment +Classifier: Framework :: Flask +Classifier: Intended Audience :: Developers +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 +License-File: LICENSE.txt +Requires-Dist: blinker>=1.9.0 +Requires-Dist: click>=8.1.3 +Requires-Dist: importlib-metadata>=3.6.0; python_version < '3.10' +Requires-Dist: itsdangerous>=2.2.0 +Requires-Dist: jinja2>=3.1.2 +Requires-Dist: markupsafe>=2.1.1 +Requires-Dist: werkzeug>=3.1.0 +Requires-Dist: asgiref>=3.2 ; extra == "async" +Requires-Dist: python-dotenv ; extra == "dotenv" +Project-URL: Changes, https://flask.palletsprojects.com/page/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/ + +## 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) +``` + +## 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 + +## 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/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/RECORD new file mode 100644 index 0000000..0174f8f --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/RECORD @@ -0,0 +1,58 @@ +../../../bin/flask,sha256=3YsdpRBia0sH9o6LqvfP1WDLeD1TD3aqyp4JkltIfKI,226 +flask-3.1.2.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +flask-3.1.2.dist-info/METADATA,sha256=oRg63DAAIcoLAr7kzTgIEKfm8_4HMTRpmWmIptdY_js,3167 +flask-3.1.2.dist-info/RECORD,, +flask-3.1.2.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask-3.1.2.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82 +flask-3.1.2.dist-info/entry_points.txt,sha256=bBP7hTOS5fz9zLtC7sPofBZAlMkEvBxu7KqS6l5lvc4,40 +flask-3.1.2.dist-info/licenses/LICENSE.txt,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +flask/__init__.py,sha256=mHvJN9Swtl1RDtjCqCIYyIniK_SZ_l_hqUynOzgpJ9o,2701 +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=XGqgFRsLgBhzIoB2HSftoMTIM3hjDiH6rdV7c3g3IKc,61744 +flask/blueprints.py,sha256=p5QE2lY18GItbdr_RKRpZ8Do17g0PvQGIgZkSUDhX2k,4541 +flask/cli.py,sha256=Pfh72-BxlvoH0QHCDOc1HvXG7Kq5Xetf3zzNz2kNSHk,37184 +flask/config.py,sha256=PiqF0DPam6HW0FH4CH1hpXTBe30NSzjPEOwrz1b6kt0,13219 +flask/ctx.py,sha256=sPKzahqtgxaS7O0y9E_NzUJNUDyTD6M4GkDrVu2fU3Y,15064 +flask/debughelpers.py,sha256=PGIDhStW_efRjpaa3zHIpo-htStJOR41Ip3OJWPYBwo,6080 +flask/globals.py,sha256=XdQZmStBmPIs8t93tjx6pO7Bm3gobAaONWkFcUHaGas,1713 +flask/helpers.py,sha256=rJZge7_J288J1UQv5-kNf4oEaw332PP8NTW0QRIBbXE,23517 +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=5imEzY5HjV2HoUVrQbJLqXCzMNpZXfD0Y1XqdLV2XBA,7672 +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=5EbxwHOchgcpZqQyalA9vyDBopknOvDg6BVwXFyFD2s,38099 +flask/sansio/blueprints.py,sha256=Tqe-7EkZ-tbWchm8iDoCfD848f0_3nLv6NNjeIPvHwM,24637 +flask/sansio/scaffold.py,sha256=wSASXYdFRWJmqcL0Xq-T7N-PDVUSiFGvjO9kPZg58bk,30371 +flask/sessions.py,sha256=duvYGmCGh_H3cgMuy2oeSjrCsCvLylF4CBKOXpN0Qms,15480 +flask/signals.py,sha256=V7lMUww7CqgJ2ThUBn1PiatZtQanOyt7OZpu2GZI-34,750 +flask/templating.py,sha256=IHsdsF-eBJPCJE0AJLCi1VhhnytOGdzHCn3yThz87c4,7536 +flask/testing.py,sha256=zzC7XxhBWOP9H697IV_4SG7Lg3Lzb5PWiyEP93_KQXE,10117 +flask/typing.py,sha256=L-L5t2jKgS0aOmVhioQ_ylqcgiVFnA6yxO-RLNhq-GU,3293 +flask/views.py,sha256=xzJx6oJqGElThtEghZN7ZQGMw5TDFyuRxUkecwRuAoA,6962 +flask/wrappers.py,sha256=jUkv4mVek2Iq4hwxd4RvqrIMb69Bv0PElDgWLmd5ORo,9406 diff --git a/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/REQUESTED b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/WHEEL new file mode 100644 index 0000000..d8b9936 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask-3.1.2.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/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/entry_points.txt b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/entry_points.txt new file mode 100644 index 0000000..eec6733 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/entry_points.txt @@ -0,0 +1,3 @@ +[console_scripts] +flask=flask.cli:main + diff --git a/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/licenses/LICENSE.txt b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask-3.1.2.dist-info/licenses/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/tapdown/lib/python3.11/site-packages/flask/__init__.py b/tapdown/lib/python3.11/site-packages/flask/__init__.py new file mode 100644 index 0000000..1fdc50c --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/__init__.py @@ -0,0 +1,61 @@ +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 + +if not t.TYPE_CHECKING: + + 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.2. Use feature detection or" + " 'importlib.metadata.version(\"flask\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("flask") + + raise AttributeError(name) diff --git a/tapdown/lib/python3.11/site-packages/flask/__main__.py b/tapdown/lib/python3.11/site-packages/flask/__main__.py new file mode 100644 index 0000000..4e28416 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/__main__.py @@ -0,0 +1,3 @@ +from .cli import main + +main() diff --git a/tapdown/lib/python3.11/site-packages/flask/app.py b/tapdown/lib/python3.11/site-packages/flask/app.py new file mode 100644 index 0000000..1232b03 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/app.py @@ -0,0 +1,1536 @@ +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 werkzeug.wsgi import get_host + +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 + from .typing import HeadersValue + +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, + "SECRET_KEY_FALLBACKS": None, + "PERMANENT_SESSION_LIFETIME": timedelta(days=31), + "USE_X_SENDFILE": False, + "TRUSTED_HOSTS": None, + "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_PARTITIONED": False, + "SESSION_COOKIE_SAMESITE": None, + "SESSION_REFRESH_EACH_REQUEST": True, + "MAX_CONTENT_LENGTH": None, + "MAX_FORM_MEMORY_SIZE": 500_000, + "MAX_FORM_PARTS": 1_000, + "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, + "PROVIDE_AUTOMATIC_OPTIONS": True, + } + ) + + #: 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", encoding: str | None = None + ) -> 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"``. + :param encoding: Open the file with this encoding when opening in text + mode. This is ignored when opening in binary mode. + + .. versionchanged:: 3.1 + Added the ``encoding`` parameter. + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + path = os.path.join(self.root_path, resource) + + if mode == "rb": + return open(path, mode) # pyright: ignore + + return open(path, mode, encoding=encoding) + + def open_instance_resource( + self, resource: str, mode: str = "rb", encoding: str | None = "utf-8" + ) -> t.IO[t.AnyStr]: + """Open a resource file relative to the application's instance folder + :attr:`instance_path`. Unlike :meth:`open_resource`, files in the + instance folder can be opened for writing. + + :param resource: Path to the resource relative to :attr:`instance_path`. + :param mode: Open the file in this mode. + :param encoding: Open the file with this encoding when opening in text + mode. This is ignored when opening in binary mode. + + .. versionchanged:: 3.1 + Added the ``encoding`` parameter. + """ + path = os.path.join(self.instance_path, resource) + + if "b" in mode: + return open(path, mode) + + return open(path, mode, encoding=encoding) + + 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. + + .. versionchanged:: 3.1 + If :data:`SERVER_NAME` is set, it does not restrict requests to + only that domain, for both ``subdomain_matching`` and + ``host_matching``. + + .. versionchanged:: 1.0 + :data:`SERVER_NAME` no longer implicitly enables subdomain + matching. Use :attr:`subdomain_matching` instead. + + .. versionchanged:: 0.9 + This can be called outside a request when the URL adapter is created + for an application context. + + .. versionadded:: 0.6 + """ + if request is not None: + if (trusted_hosts := self.config["TRUSTED_HOSTS"]) is not None: + request.trusted_hosts = trusted_hosts + + # Check trusted_hosts here until bind_to_environ does. + request.host = get_host(request.environ, request.trusted_hosts) # pyright: ignore + subdomain = None + server_name = self.config["SERVER_NAME"] + + if self.url_map.host_matching: + # Don't pass SERVER_NAME, otherwise it's used and the actual + # host is ignored, which breaks host matching. + server_name = None + elif not self.subdomain_matching: + # Werkzeug doesn't implement subdomain matching yet. Until then, + # disable it by forcing the current subdomain to the default, or + # the empty string. + subdomain = self.url_map.default_subdomain or "" + + return self.url_map.bind_to_environ( + request.environ, server_name=server_name, subdomain=subdomain + ) + + # Need at least SERVER_NAME to match/build outside a request. + 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: int | None = None + headers: HeadersValue | None = 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 # pyright: ignore + 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, # pyright: ignore + 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) + + 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/tapdown/lib/python3.11/site-packages/flask/blueprints.py b/tapdown/lib/python3.11/site-packages/flask/blueprints.py new file mode 100644 index 0000000..b6d4e43 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/blueprints.py @@ -0,0 +1,128 @@ +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", encoding: str | None = "utf-8" + ) -> t.IO[t.AnyStr]: + """Open a resource file relative to :attr:`root_path` for reading. The + blueprint-relative equivalent of the app's :meth:`~.Flask.open_resource` + method. + + :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"``. + :param encoding: Open the file with this encoding when opening in text + mode. This is ignored when opening in binary mode. + + .. versionchanged:: 3.1 + Added the ``encoding`` parameter. + """ + if mode not in {"r", "rt", "rb"}: + raise ValueError("Resources can only be opened for reading.") + + path = os.path.join(self.root_path, resource) + + if mode == "rb": + return open(path, mode) # pyright: ignore + + return open(path, mode, encoding=encoding) diff --git a/tapdown/lib/python3.11/site-packages/flask/cli.py b/tapdown/lib/python3.11/site-packages/flask/cli.py new file mode 100644 index 0000000..ed11f25 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/cli.py @@ -0,0 +1,1135 @@ +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. + + .. versionchanged:: 3.1 + Added the ``load_dotenv_defaults`` parameter and attribute. + """ + + def __init__( + self, + app_import_path: str | None = None, + create_app: t.Callable[..., Flask] | None = None, + set_debug_flag: bool = True, + load_dotenv_defaults: 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.load_dotenv_defaults = get_load_dotenv(load_dotenv_defaults) + """Whether default ``.flaskenv`` and ``.env`` files should be loaded. + + ``ScriptInfo`` doesn't load anything, this is for reference when doing + the load elsewhere during processing. + + .. versionadded:: 3.1 + """ + + 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 + app: Flask | None = None + if self.create_app is not None: + app = 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: + try: + import dotenv # noqa: F401 + except ImportError: + # Only show an error if a value was passed, otherwise we still want to + # call load_dotenv and show a message without exiting. + if value is not None: + raise click.BadParameter( + "python-dotenv must be installed to load an env file.", + ctx=ctx, + param=param, + ) from None + + # Load if a value was passed, or we want to load default files, or both. + if value is not None or ctx.obj.load_dotenv_defaults: + load_dotenv(value, load_defaults=ctx.obj.load_dotenv_defaults) + + 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, taking precedence over" + " those set by '.env' and '.flaskenv'. Variables set directly in the" + " environment take highest precedence. 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:: 3.1 + ``-e path`` takes precedence over default ``.env`` and ``.flaskenv`` files. + + .. 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[click.Parameter] = 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 # pyright: ignore + + 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" + + 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, + load_dotenv_defaults=self.load_dotenv, + ) + + 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) or ( + len(args) == 1 and args[0] in self.get_help_option_names(ctx) + ): + # 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, load_defaults: bool = True +) -> bool: + """Load "dotenv" files to set environment variables. A given path takes + precedence over ``.env``, which takes precedence over ``.flaskenv``. After + loading and combining these files, values are only set if the key is not + already set in ``os.environ``. + + 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. + :param load_defaults: Search for and load the default ``.flaskenv`` and + ``.env`` files. + :return: ``True`` if at least one env var was loaded. + + .. versionchanged:: 3.1 + Added the ``load_defaults`` parameter. A given path takes precedence + over default files. + + .. 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 files present. Install python-dotenv" + " to use them.", + fg="yellow", + err=True, + ) + + return False + + data: dict[str, str | None] = {} + + if load_defaults: + for default_name in (".flaskenv", ".env"): + if not (default_path := dotenv.find_dotenv(default_name, usecwd=True)): + continue + + data |= dotenv.dotenv_values(default_path, encoding="utf-8") + + if path is not None and os.path.isfile(path): + data |= dotenv.dotenv_values(path, encoding="utf-8") + + for key, value in data.items(): + if key in os.environ or value is None: + continue + + os.environ[key] = value + + return bool(data) # True if at least one env var was 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() # pyright: ignore + 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/tapdown/lib/python3.11/site-packages/flask/config.py b/tapdown/lib/python3.11/site-packages/flask/config.py new file mode 100644 index 0000000..34ef1a5 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/config.py @@ -0,0 +1,367 @@ +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}_" + + for key in sorted(os.environ): + if not key.startswith(prefix): + continue + + value = os.environ[key] + key = key.removeprefix(prefix) + + try: + value = loads(value) + except Exception: + # Keep the value as a string if loading failed. + pass + + 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/tapdown/lib/python3.11/site-packages/flask/ctx.py b/tapdown/lib/python3.11/site-packages/flask/ctx.py new file mode 100644 index 0000000..222e818 --- /dev/null +++ b/tapdown/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: + return ctx.app.ensure_sync(f)(*args, **kwargs) + + 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/tapdown/lib/python3.11/site-packages/flask/debughelpers.py b/tapdown/lib/python3.11/site-packages/flask/debughelpers.py new file mode 100644 index 0000000..2c8c4c4 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/flask/globals.py b/tapdown/lib/python3.11/site-packages/flask/globals.py new file mode 100644 index 0000000..e2c410c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/flask/helpers.py b/tapdown/lib/python3.11/site-packages/flask/helpers.py new file mode 100644 index 0000000..5d412c9 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/helpers.py @@ -0,0 +1,641 @@ +from __future__ import annotations + +import importlib.util +import os +import sys +import typing as t +from datetime import datetime +from functools import 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_app +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") + + +@t.overload +def stream_with_context( + generator_or_function: t.Iterator[t.AnyStr], +) -> t.Iterator[t.AnyStr]: ... + + +@t.overload +def stream_with_context( + generator_or_function: t.Callable[..., t.Iterator[t.AnyStr]], +) -> t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: ... + + +def stream_with_context( + generator_or_function: t.Iterator[t.AnyStr] | t.Callable[..., t.Iterator[t.AnyStr]], +) -> t.Iterator[t.AnyStr] | t.Callable[[t.Iterator[t.AnyStr]], t.Iterator[t.AnyStr]]: + """Wrap a response generator function so that it runs inside the current + request context. This keeps :data:`request`, :data:`session`, and :data:`g` + available, even though at the point the generator runs the request context + will typically have ended. + + Use it as a decorator on a generator function: + + .. code-block:: python + + from flask import stream_with_context, request, Response + + @app.get("/stream") + def streamed_response(): + @stream_with_context + def generate(): + yield "Hello " + yield request.args["name"] + yield "!" + + return Response(generate()) + + Or use it as a wrapper around a created generator: + + .. code-block:: python + + from flask import stream_with_context, request, Response + + @app.get("/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]: + if (req_ctx := _cv_request.get(None)) is None: + raise RuntimeError( + "'stream_with_context' can only be used when a request" + " context is active, such as in a view function." + ) + + app_ctx = _cv_app.get() + # Setup code below will run the generator to this point, so that the + # current contexts are recorded. The contexts must be pushed after, + # otherwise their ContextVar will record the wrong event loop during + # async view functions. + yield None # type: ignore[misc] + + # Push the app context first, so that the request context does not + # automatically create and push a different app context. + with app_ctx, req_ctx: + try: + yield from gen + finally: + # Clean up in case the user wrapped a WSGI iterator. + if hasattr(gen, "close"): + gen.close() + + # Execute the generator to the sentinel value. This ensures the context is + # preserved in the generator's state. Further iteration will push the + # context and yield from the original iterator. + wrapped_g = generator() + next(wrapped_g) + return wrapped_g + + +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, + ) + return kwargs + + +def send_file( + path_or_file: os.PathLike[t.AnyStr] | str | t.IO[bytes], + 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. This *must not* + be a value provided by the client, otherwise it becomes insecure. + :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) # pyright: ignore + 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] + + +@cache +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/tapdown/lib/python3.11/site-packages/flask/json/__init__.py b/tapdown/lib/python3.11/site-packages/flask/json/__init__.py new file mode 100644 index 0000000..c0941d0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/flask/json/provider.py b/tapdown/lib/python3.11/site-packages/flask/json/provider.py new file mode 100644 index 0000000..ea7e475 --- /dev/null +++ b/tapdown/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) # type: ignore[arg-type] + + 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/tapdown/lib/python3.11/site-packages/flask/json/tag.py b/tapdown/lib/python3.11/site-packages/flask/json/tag.py new file mode 100644 index 0000000..8dc3629 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/flask/logging.py b/tapdown/lib/python3.11/site-packages/flask/logging.py new file mode 100644 index 0000000..0cb8f43 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/flask/py.typed b/tapdown/lib/python3.11/site-packages/flask/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/flask/sansio/README.md b/tapdown/lib/python3.11/site-packages/flask/sansio/README.md new file mode 100644 index 0000000..623ac19 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/flask/sansio/app.py b/tapdown/lib/python3.11/site-packages/flask/sansio/app.py new file mode 100644 index 0000000..a2592fe --- /dev/null +++ b/tapdown/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, + ) -> 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: + """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 Jinja 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[str] = 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 and self.config["PROVIDE_AUTOMATIC_OPTIONS"]: + 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/tapdown/lib/python3.11/site-packages/flask/sansio/blueprints.py b/tapdown/lib/python3.11/site-packages/flask/sansio/blueprints.py new file mode 100644 index 0000000..4f912cc --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/flask/sansio/scaffold.py b/tapdown/lib/python3.11/site-packages/flask/sansio/scaffold.py new file mode 100644 index 0000000..0e96f15 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/sansio/scaffold.py @@ -0,0 +1,792 @@ +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 + 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 _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 package_path.is_relative_to(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 pathlib.PurePath(package_path).is_relative_to(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/tapdown/lib/python3.11/site-packages/flask/sessions.py b/tapdown/lib/python3.11/site-packages/flask/sessions.py new file mode 100644 index 0000000..0a357d9 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/sessions.py @@ -0,0 +1,399 @@ +from __future__ import annotations + +import collections.abc as c +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 + + +class SessionMixin(MutableMapping[str, t.Any]): + """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 + + +class SecureCookieSession(CallbackDict[str, t.Any], SessionMixin): + """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: c.Mapping[str, t.Any] | c.Iterable[tuple[str, t.Any]] | None = 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 # 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_cookie_partitioned(self, app: Flask) -> bool: + """Returns True if the cookie should be partitioned. By default, uses + the value of :data:`SESSION_COOKIE_PARTITIONED`. + + .. versionadded:: 3.1 + """ + return app.config["SESSION_COOKIE_PARTITIONED"] # 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 + + keys: list[str | bytes] = [] + + if fallbacks := app.config["SECRET_KEY_FALLBACKS"]: + keys.extend(fallbacks) + + keys.append(app.secret_key) # itsdangerous expects current key at top + return URLSafeTimedSerializer( + keys, # type: ignore[arg-type] + salt=self.salt, + serializer=self.serializer, + signer_kwargs={ + "key_derivation": self.key_derivation, + "digest_method": self.digest_method, + }, + ) + + 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) + partitioned = self.get_cookie_partitioned(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, + partitioned=partitioned, + 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[union-attr] + response.set_cookie( + name, + val, + expires=expires, + httponly=httponly, + domain=domain, + path=path, + secure=secure, + partitioned=partitioned, + samesite=samesite, + ) + response.vary.add("Cookie") diff --git a/tapdown/lib/python3.11/site-packages/flask/signals.py b/tapdown/lib/python3.11/site-packages/flask/signals.py new file mode 100644 index 0000000..444fda9 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/flask/templating.py b/tapdown/lib/python3.11/site-packages/flask/templating.py new file mode 100644 index 0000000..16d480f --- /dev/null +++ b/tapdown/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 Jinja 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/tapdown/lib/python3.11/site-packages/flask/testing.py b/tapdown/lib/python3.11/site-packages/flask/testing.py new file mode 100644 index 0000000..55eb12f --- /dev/null +++ b/tapdown/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 click.testing import Result +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: + path = f"{path}?{url.query}" + + self.app = app + super().__init__(path, base_url, *args, **kwargs) + + def json_dumps(self, obj: t.Any, **kwargs: t.Any) -> str: + """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. + for cm in self._new_contexts: + self._context_stack.enter_context(cm) + + self._new_contexts.clear() + 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 + ) -> Result: + """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/tapdown/lib/python3.11/site-packages/flask/typing.py b/tapdown/lib/python3.11/site-packages/flask/typing.py new file mode 100644 index 0000000..6b70c40 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/typing.py @@ -0,0 +1,93 @@ +from __future__ import annotations + +import collections.abc as cabc +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, + list[t.Any], + # Only dict is actually accepted, but Mapping allows for TypedDict. + t.Mapping[str, t.Any], + t.Iterator[str], + t.Iterator[bytes], + cabc.AsyncIterable[str], # for Quart, until App is generic. + cabc.AsyncIterable[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, list[str], tuple[str, ...]] + +# the possible types for HTTP headers +HeadersValue = t.Union[ + "Headers", + t.Mapping[str, HeaderValue], + t.Sequence[tuple[str, HeaderValue]], +] + +# The possible types returned by a route function. +ResponseReturnValue = t.Union[ + ResponseValue, + tuple[ResponseValue, HeadersValue], + tuple[ResponseValue, int], + 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[[], 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[[], dict[str, t.Any]], + t.Callable[[], t.Awaitable[dict[str, t.Any]]], +] +TemplateFilterCallable = t.Callable[..., t.Any] +TemplateGlobalCallable = t.Callable[..., t.Any] +TemplateTestCallable = t.Callable[..., bool] +URLDefaultCallable = t.Callable[[str, dict[str, t.Any]], None] +URLValuePreprocessorCallable = t.Callable[ + [t.Optional[str], t.Optional[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/tapdown/lib/python3.11/site-packages/flask/views.py b/tapdown/lib/python3.11/site-packages/flask/views.py new file mode 100644 index 0000000..53fe976 --- /dev/null +++ b/tapdown/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[..., t.Any]]] = [] + + #: 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) # pyright: ignore + + 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/tapdown/lib/python3.11/site-packages/flask/wrappers.py b/tapdown/lib/python3.11/site-packages/flask/wrappers.py new file mode 100644 index 0000000..bab6102 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask/wrappers.py @@ -0,0 +1,257 @@ +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 + + _max_content_length: int | None = None + _max_form_memory_size: int | None = None + _max_form_parts: int | None = None + + @property + def max_content_length(self) -> int | None: + """The maximum number of bytes that will be read during this request. If + this limit is exceeded, a 413 :exc:`~werkzeug.exceptions.RequestEntityTooLarge` + error is raised. If it is set to ``None``, no limit is enforced at the + Flask application level. However, if it is ``None`` and the request has + no ``Content-Length`` header and the WSGI server does not indicate that + it terminates the stream, then no data is read to avoid an infinite + stream. + + Each request defaults to the :data:`MAX_CONTENT_LENGTH` config, which + defaults to ``None``. It can be set on a specific ``request`` to apply + the limit to that specific view. This should be set appropriately based + on an application's or view's specific needs. + + .. versionchanged:: 3.1 + This can be set per-request. + + .. versionchanged:: 0.6 + This is configurable through Flask config. + """ + if self._max_content_length is not None: + return self._max_content_length + + if not current_app: + return super().max_content_length + + return current_app.config["MAX_CONTENT_LENGTH"] # type: ignore[no-any-return] + + @max_content_length.setter + def max_content_length(self, value: int | None) -> None: + self._max_content_length = value + + @property + def max_form_memory_size(self) -> int | None: + """The maximum size in bytes any non-file form field may be in a + ``multipart/form-data`` body. If this limit is exceeded, a 413 + :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it + is set to ``None``, no limit is enforced at the Flask application level. + + Each request defaults to the :data:`MAX_FORM_MEMORY_SIZE` config, which + defaults to ``500_000``. It can be set on a specific ``request`` to + apply the limit to that specific view. This should be set appropriately + based on an application's or view's specific needs. + + .. versionchanged:: 3.1 + This is configurable through Flask config. + """ + if self._max_form_memory_size is not None: + return self._max_form_memory_size + + if not current_app: + return super().max_form_memory_size + + return current_app.config["MAX_FORM_MEMORY_SIZE"] # type: ignore[no-any-return] + + @max_form_memory_size.setter + def max_form_memory_size(self, value: int | None) -> None: + self._max_form_memory_size = value + + @property # type: ignore[override] + def max_form_parts(self) -> int | None: + """The maximum number of fields that may be present in a + ``multipart/form-data`` body. If this limit is exceeded, a 413 + :exc:`~werkzeug.exceptions.RequestEntityTooLarge` error is raised. If it + is set to ``None``, no limit is enforced at the Flask application level. + + Each request defaults to the :data:`MAX_FORM_PARTS` config, which + defaults to ``1_000``. It can be set on a specific ``request`` to apply + the limit to that specific view. This should be set appropriately based + on an application's or view's specific needs. + + .. versionchanged:: 3.1 + This is configurable through Flask config. + """ + if self._max_form_parts is not None: + return self._max_form_parts + + if not current_app: + return super().max_form_parts + + return current_app.config["MAX_FORM_PARTS"] # type: ignore[no-any-return] + + @max_form_parts.setter + def max_form_parts(self, value: int | None) -> None: + self._max_form_parts = value + + @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 # type: ignore[no-any-return] + + 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 ebr: + if current_app and current_app.debug: + raise + + raise BadRequest() from ebr + + +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/tapdown/lib/python3.11/site-packages/flask_socketio/__init__.py b/tapdown/lib/python3.11/site-packages/flask_socketio/__init__.py new file mode 100644 index 0000000..219f30c --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_socketio/__init__.py @@ -0,0 +1,1125 @@ +from functools import wraps +import os +import sys + +# make sure gevent-socketio is not installed, as it conflicts with +# python-socketio +gevent_socketio_found = True +try: + from socketio import socketio_manage # noqa: F401 +except ImportError: + gevent_socketio_found = False +if gevent_socketio_found: + print('The gevent-socketio package is incompatible with this version of ' + 'the Flask-SocketIO extension. Please uninstall it, and then ' + 'install the latest version of python-socketio in its place.') + sys.exit(1) + +import flask +from flask import has_request_context, json as flask_json +from flask.sessions import SessionMixin +import socketio +from socketio.exceptions import ConnectionRefusedError # noqa: F401 +from werkzeug.debug import DebuggedApplication +from werkzeug._reloader import run_with_reloader + +from .namespace import Namespace +from .test_client import SocketIOTestClient + + +class _SocketIOMiddleware(socketio.WSGIApp): + """This WSGI middleware simply exposes the Flask application in the WSGI + environment before executing the request. + """ + def __init__(self, socketio_app, flask_app, socketio_path='socket.io'): + self.flask_app = flask_app + super().__init__(socketio_app, flask_app.wsgi_app, + socketio_path=socketio_path) + + def __call__(self, environ, start_response): + environ = environ.copy() + environ['flask.app'] = self.flask_app + return super().__call__(environ, start_response) + + +class _ManagedSession(dict, SessionMixin): + """This class is used for user sessions that are managed by + Flask-SocketIO. It is simple dict, expanded with the Flask session + attributes.""" + pass + + +class SocketIO: + """Create a Flask-SocketIO server. + + :param app: The flask application instance. If the application instance + isn't known at the time this class is instantiated, then call + ``socketio.init_app(app)`` once the application instance is + available. + :param manage_session: If set to ``True``, this extension manages the user + session for Socket.IO events. If set to ``False``, + Flask's own session management is used. When using + Flask's cookie based sessions it is recommended that + you leave this set to the default of ``True``. When + using server-side sessions, a ``False`` setting + enables sharing the user session between HTTP routes + and Socket.IO events. + :param message_queue: A connection URL for a message queue service the + server can use for multi-process communication. A + message queue is not required when using a single + server process. + :param channel: The channel name, when using a message queue. If a channel + isn't specified, a default channel will be used. If + multiple clusters of SocketIO processes need to use the + same message queue without interfering with each other, + then each cluster should use a different channel. + :param path: The path where the Socket.IO server is exposed. Defaults to + ``'socket.io'``. Leave this as is unless you know what you are + doing. + :param resource: Alias to ``path``. + :param kwargs: Socket.IO and Engine.IO server options. + + The Socket.IO server options are detailed below: + + :param client_manager: The client manager instance that will manage the + client list. When this is omitted, the client list + is stored in an in-memory structure, so the use of + multiple connected servers is not possible. In most + cases, this argument does not need to be set + explicitly. + :param logger: To enable logging set to ``True`` or pass a logger object to + use. To disable logging set to ``False``. The default is + ``False``. Note that fatal errors will be logged even when + ``logger`` is ``False``. + :param json: An alternative json module to use for encoding and decoding + packets. Custom json modules must have ``dumps`` and ``loads`` + functions that are compatible with the standard library + versions. To use the same json encoder and decoder as a Flask + application, use ``flask.json``. + :param async_handlers: If set to ``True``, event handlers for a client are + executed in separate threads. To run handlers for a + client synchronously, set to ``False``. The default + is ``True``. + :param always_connect: When set to ``False``, new connections are + provisory until the connect handler returns + something other than ``False``, at which point they + are accepted. When set to ``True``, connections are + immediately accepted, and then if the connect + handler returns ``False`` a disconnect is issued. + Set to ``True`` if you need to emit events from the + connect handler and your client is confused when it + receives events before the connection acceptance. + In any other case use the default of ``False``. + + The Engine.IO server configuration supports the following settings: + + :param async_mode: The asynchronous model to use. See the Deployment + section in the documentation for a description of the + available options. Valid async modes are ``threading``, + ``eventlet``, ``gevent`` and ``gevent_uwsgi``. If this + argument is not given, ``eventlet`` is tried first, then + ``gevent_uwsgi``, then ``gevent``, and finally + ``threading``. The first async mode that has all its + dependencies installed is then one that is chosen. + :param ping_interval: The interval in seconds at which the server pings + the client. The default is 25 seconds. For advanced + control, a two element tuple can be given, where + the first number is the ping interval and the second + is a grace period added by the server. + :param ping_timeout: The time in seconds that the client waits for the + server to respond before disconnecting. The default + is 5 seconds. + :param max_http_buffer_size: The maximum size of a message when using the + polling transport. The default is 1,000,000 + bytes. + :param allow_upgrades: Whether to allow transport upgrades or not. The + default is ``True``. + :param http_compression: Whether to compress packages when using the + polling transport. The default is ``True``. + :param compression_threshold: Only compress messages when their byte size + is greater than this value. The default is + 1024 bytes. + :param cookie: If set to a string, it is the name of the HTTP cookie the + server sends back to the client containing the client + session id. If set to a dictionary, the ``'name'`` key + contains the cookie name and other keys define cookie + attributes, where the value of each attribute can be a + string, a callable with no arguments, or a boolean. If set + to ``None`` (the default), a cookie is not sent to the + client. + :param cors_allowed_origins: Origin or list of origins that are allowed to + connect to this server. Only the same origin + is allowed by default. Set this argument to + ``'*'`` to allow all origins, or to ``[]`` to + disable CORS handling. + :param cors_credentials: Whether credentials (cookies, authentication) are + allowed in requests to this server. The default is + ``True``. + :param monitor_clients: If set to ``True``, a background task will ensure + inactive clients are closed. Set to ``False`` to + disable the monitoring task (not recommended). The + default is ``True``. + :param engineio_logger: To enable Engine.IO logging set to ``True`` or pass + a logger object to use. To disable logging set to + ``False``. The default is ``False``. Note that + fatal errors are logged even when + ``engineio_logger`` is ``False``. + """ + reason = socketio.Server.reason + + def __init__(self, app=None, **kwargs): + self.server = None + self.server_options = {} + self.wsgi_server = None + self.handlers = [] + self.namespace_handlers = [] + self.exception_handlers = {} + self.default_exception_handler = None + self.manage_session = True + # We can call init_app when: + # - we were given the Flask app instance (standard initialization) + # - we were not given the app, but we were given a message_queue + # (standard initialization for auxiliary process) + # In all other cases we collect the arguments and assume the client + # will call init_app from an app factory function. + if app is not None or 'message_queue' in kwargs: + self.init_app(app, **kwargs) + else: + self.server_options.update(kwargs) + + def init_app(self, app, **kwargs): + if app is not None: + if not hasattr(app, 'extensions'): + app.extensions = {} # pragma: no cover + app.extensions['socketio'] = self + self.server_options.update(kwargs) + self.manage_session = self.server_options.pop('manage_session', + self.manage_session) + + if 'client_manager' not in kwargs: + url = self.server_options.get('message_queue', None) + channel = self.server_options.pop('channel', 'flask-socketio') + write_only = app is None + if url: + if url.startswith(('redis://', "rediss://")): + queue_class = socketio.RedisManager + elif url.startswith('kafka://'): + queue_class = socketio.KafkaManager + elif url.startswith('zmq'): + queue_class = socketio.ZmqManager + else: + queue_class = socketio.KombuManager + queue = queue_class(url, channel=channel, + write_only=write_only) + self.server_options['client_manager'] = queue + + if 'json' in self.server_options and \ + self.server_options['json'] == flask_json: + # flask's json module is tricky to use because its output + # changes when it is invoked inside or outside the app context + # so here to prevent any ambiguities we replace it with wrappers + # that ensure that the app context is always present + class FlaskSafeJSON: + @staticmethod + def dumps(*args, **kwargs): + with app.app_context(): + return flask_json.dumps(*args, **kwargs) + + @staticmethod + def loads(*args, **kwargs): + with app.app_context(): + return flask_json.loads(*args, **kwargs) + + self.server_options['json'] = FlaskSafeJSON + + resource = self.server_options.pop('path', None) or \ + self.server_options.pop('resource', None) or 'socket.io' + if resource.startswith('/'): + resource = resource[1:] + if os.environ.get('FLASK_RUN_FROM_CLI'): + if self.server_options.get('async_mode') is None: + self.server_options['async_mode'] = 'threading' + self.server = socketio.Server(**self.server_options) + self.async_mode = self.server.async_mode + for handler in self.handlers: + self.server.on(handler[0], handler[1], namespace=handler[2]) + for namespace_handler in self.namespace_handlers: + self.server.register_namespace(namespace_handler) + + if app is not None: + # here we attach the SocketIO middleware to the SocketIO object so + # it can be referenced later if debug middleware needs to be + # inserted + self.sockio_mw = _SocketIOMiddleware(self.server, app, + socketio_path=resource) + app.wsgi_app = self.sockio_mw + + def on(self, message, namespace=None): + """Decorator to register a SocketIO event handler. + + This decorator must be applied to SocketIO event handlers. Example:: + + @socketio.on('my event', namespace='/chat') + def handle_my_custom_event(json): + print('received json: ' + str(json)) + + :param message: The name of the event. This is normally a user defined + string, but a few event names are already defined. Use + ``'message'`` to define a handler that takes a string + payload, ``'json'`` to define a handler that takes a + JSON blob payload, ``'connect'`` or ``'disconnect'`` + to create handlers for connection and disconnection + events. + :param namespace: The namespace on which the handler is to be + registered. Defaults to the global namespace. + """ + namespace = namespace or '/' + + def decorator(handler): + @wraps(handler) + def _handler(sid, *args): + nonlocal namespace + real_ns = namespace + if namespace == '*': + real_ns = sid + sid = args[0] + args = args[1:] + real_msg = message + if message == '*': + real_msg = sid + sid = args[0] + args = [real_msg] + list(args[1:]) + return self._handle_event(handler, message, real_ns, sid, + *args) + + if self.server: + self.server.on(message, _handler, namespace=namespace) + else: + self.handlers.append((message, _handler, namespace)) + return handler + return decorator + + def on_error(self, namespace=None): + """Decorator to define a custom error handler for SocketIO events. + + This decorator can be applied to a function that acts as an error + handler for a namespace. This handler will be invoked when a SocketIO + event handler raises an exception. The handler function must accept one + argument, which is the exception raised. Example:: + + @socketio.on_error(namespace='/chat') + def chat_error_handler(e): + print('An error has occurred: ' + str(e)) + + :param namespace: The namespace for which to register the error + handler. Defaults to the global namespace. + """ + namespace = namespace or '/' + + def decorator(exception_handler): + if not callable(exception_handler): + raise ValueError('exception_handler must be callable') + self.exception_handlers[namespace] = exception_handler + return exception_handler + return decorator + + def on_error_default(self, exception_handler): + """Decorator to define a default error handler for SocketIO events. + + This decorator can be applied to a function that acts as a default + error handler for any namespaces that do not have a specific handler. + Example:: + + @socketio.on_error_default + def error_handler(e): + print('An error has occurred: ' + str(e)) + """ + if not callable(exception_handler): + raise ValueError('exception_handler must be callable') + self.default_exception_handler = exception_handler + return exception_handler + + def on_event(self, message, handler, namespace=None): + """Register a SocketIO event handler. + + ``on_event`` is the non-decorator version of ``'on'``. + + Example:: + + def on_foo_event(json): + print('received json: ' + str(json)) + + socketio.on_event('my event', on_foo_event, namespace='/chat') + + :param message: The name of the event. This is normally a user defined + string, but a few event names are already defined. Use + ``'message'`` to define a handler that takes a string + payload, ``'json'`` to define a handler that takes a + JSON blob payload, ``'connect'`` or ``'disconnect'`` + to create handlers for connection and disconnection + events. + :param handler: The function that handles the event. + :param namespace: The namespace on which the handler is to be + registered. Defaults to the global namespace. + """ + self.on(message, namespace=namespace)(handler) + + def event(self, *args, **kwargs): + """Decorator to register an event handler. + + This is a simplified version of the ``on()`` method that takes the + event name from the decorated function. + + Example usage:: + + @socketio.event + def my_event(data): + print('Received data: ', data) + + The above example is equivalent to:: + + @socketio.on('my_event') + def my_event(data): + print('Received data: ', data) + + A custom namespace can be given as an argument to the decorator:: + + @socketio.event(namespace='/test') + def my_event(data): + print('Received data: ', data) + """ + if len(args) == 1 and len(kwargs) == 0 and callable(args[0]): + # the decorator was invoked without arguments + # args[0] is the decorated function + return self.on(args[0].__name__)(args[0]) + else: + # the decorator was invoked with arguments + def set_handler(handler): + return self.on(handler.__name__, *args, **kwargs)(handler) + + return set_handler + + def on_namespace(self, namespace_handler): + if not isinstance(namespace_handler, Namespace): + raise ValueError('Not a namespace instance.') + namespace_handler._set_socketio(self) + if self.server: + self.server.register_namespace(namespace_handler) + else: + self.namespace_handlers.append(namespace_handler) + + def emit(self, event, *args, **kwargs): + """Emit a server generated SocketIO event. + + This function emits a SocketIO event to one or more connected clients. + A JSON blob can be attached to the event as payload. This function can + be used outside of a SocketIO event context, so it is appropriate to + use when the server is the originator of an event, outside of any + client context, such as in a regular HTTP request handler or a + background task. Example:: + + @app.route('/ping') + def ping(): + socketio.emit('ping event', {'data': 42}, namespace='/chat') + + :param event: The name of the user event to emit. + :param args: A dictionary with the JSON data to send as payload. + :param namespace: The namespace under which the message is to be sent. + Defaults to the global namespace. + :param to: Send the message to all the users in the given room, or to + the user with the given session ID. If this parameter is not + included, the event is sent to all connected users. + :param include_self: ``True`` to include the sender when broadcasting + or addressing a room, or ``False`` to send to + everyone but the sender. + :param skip_sid: The session id of a client to ignore when broadcasting + or addressing a room. This is typically set to the + originator of the message, so that everyone except + that client receive the message. To skip multiple sids + pass a list. + :param callback: If given, this function will be called to acknowledge + that the client has received the message. The + arguments that will be passed to the function are + those provided by the client. Callback functions can + only be used when addressing an individual client. + """ + namespace = kwargs.pop('namespace', '/') + to = kwargs.pop('to', None) or kwargs.pop('room', None) + include_self = kwargs.pop('include_self', True) + skip_sid = kwargs.pop('skip_sid', None) + if not include_self and not skip_sid: + skip_sid = flask.request.sid + callback = kwargs.pop('callback', None) + if callback: + # wrap the callback so that it sets app app and request contexts + sid = None + original_callback = callback + original_namespace = namespace + if has_request_context(): + sid = getattr(flask.request, 'sid', None) + original_namespace = getattr(flask.request, 'namespace', None) + + def _callback_wrapper(*args): + return self._handle_event(original_callback, None, + original_namespace, sid, *args) + + if sid: + # the callback wrapper above will install a request context + # before invoking the original callback + # we only use it if the emit was issued from a Socket.IO + # populated request context (i.e. request.sid is defined) + callback = _callback_wrapper + self.server.emit(event, *args, namespace=namespace, to=to, + skip_sid=skip_sid, callback=callback, **kwargs) + + def call(self, event, *args, **kwargs): # pragma: no cover + """Emit a SocketIO event and wait for the response. + + This method issues an emit with a callback and waits for the callback + to be invoked by the client before returning. If the callback isn’t + invoked before the timeout, then a TimeoutError exception is raised. If + the Socket.IO connection drops during the wait, this method still waits + until the specified timeout. Example:: + + def get_status(client, data): + status = call('status', {'data': data}, to=client) + + :param event: The name of the user event to emit. + :param args: A dictionary with the JSON data to send as payload. + :param namespace: The namespace under which the message is to be sent. + Defaults to the global namespace. + :param to: The session ID of the recipient client. + :param timeout: The waiting timeout. If the timeout is reached before + the client acknowledges the event, then a + ``TimeoutError`` exception is raised. The default is 60 + seconds. + :param ignore_queue: Only used when a message queue is configured. If + set to ``True``, the event is emitted to the + client directly, without going through the queue. + This is more efficient, but only works when a + single server process is used, or when there is a + single addressee. It is recommended to always + leave this parameter with its default value of + ``False``. + """ + namespace = kwargs.pop('namespace', '/') + to = kwargs.pop('to', None) or kwargs.pop('room', None) + return self.server.call(event, *args, namespace=namespace, to=to, + **kwargs) + + def send(self, data, json=False, namespace=None, to=None, + callback=None, include_self=True, skip_sid=None, **kwargs): + """Send a server-generated SocketIO message. + + This function sends a simple SocketIO message to one or more connected + clients. The message can be a string or a JSON blob. This is a simpler + version of ``emit()``, which should be preferred. This function can be + used outside of a SocketIO event context, so it is appropriate to use + when the server is the originator of an event. + + :param data: The message to send, either a string or a JSON blob. + :param json: ``True`` if ``message`` is a JSON blob, ``False`` + otherwise. + :param namespace: The namespace under which the message is to be sent. + Defaults to the global namespace. + :param to: Send the message to all the users in the given room, or to + the user with the given session ID. If this parameter is not + included, the event is sent to all connected users. + :param include_self: ``True`` to include the sender when broadcasting + or addressing a room, or ``False`` to send to + everyone but the sender. + :param skip_sid: The session id of a client to ignore when broadcasting + or addressing a room. This is typically set to the + originator of the message, so that everyone except + that client receive the message. To skip multiple sids + pass a list. + :param callback: If given, this function will be called to acknowledge + that the client has received the message. The + arguments that will be passed to the function are + those provided by the client. Callback functions can + only be used when addressing an individual client. + """ + skip_sid = flask.request.sid if not include_self else skip_sid + if json: + self.emit('json', data, namespace=namespace, to=to, + skip_sid=skip_sid, callback=callback, **kwargs) + else: + self.emit('message', data, namespace=namespace, to=to, + skip_sid=skip_sid, callback=callback, **kwargs) + + def close_room(self, room, namespace=None): + """Close a room. + + This function removes any users that are in the given room and then + deletes the room from the server. This function can be used outside + of a SocketIO event context. + + :param room: The name of the room to close. + :param namespace: The namespace under which the room exists. Defaults + to the global namespace. + """ + self.server.close_room(room, namespace) + + def run(self, app, host=None, port=None, **kwargs): # pragma: no cover + """Run the SocketIO web server. + + :param app: The Flask application instance. + :param host: The hostname or IP address for the server to listen on. + Defaults to 127.0.0.1. + :param port: The port number for the server to listen on. Defaults to + 5000. + :param debug: ``True`` to start the server in debug mode, ``False`` to + start in normal mode. + :param use_reloader: ``True`` to enable the Flask reloader, ``False`` + to disable it. + :param reloader_options: A dictionary with options that are passed to + the Flask reloader, such as ``extra_files``, + ``reloader_type``, etc. + :param extra_files: A list of additional files that the Flask + reloader should watch. Defaults to ``None``. + Deprecated, use ``reloader_options`` instead. + :param log_output: If ``True``, the server logs all incoming + connections. If ``False`` logging is disabled. + Defaults to ``True`` in debug mode, ``False`` + in normal mode. Unused when the threading async + mode is used. + :param allow_unsafe_werkzeug: Set to ``True`` to allow the use of the + Werkzeug web server in a production + setting. Default is ``False``. Set to + ``True`` at your own risk. + :param kwargs: Additional web server options. The web server options + are specific to the server used in each of the supported + async modes. Note that options provided here will + not be seen when using an external web server such + as gunicorn, since this method is not called in that + case. + """ + if host is None: + host = '127.0.0.1' + if port is None: + server_name = app.config['SERVER_NAME'] + if server_name and ':' in server_name: + port = int(server_name.rsplit(':', 1)[1]) + else: + port = 5000 + + debug = kwargs.pop('debug', app.debug) + log_output = kwargs.pop('log_output', debug) + use_reloader = kwargs.pop('use_reloader', debug) + extra_files = kwargs.pop('extra_files', None) + reloader_options = kwargs.pop('reloader_options', {}) + if extra_files: + reloader_options['extra_files'] = extra_files + + app.debug = debug + if app.debug and self.server.eio.async_mode != 'threading': + # put the debug middleware between the SocketIO middleware + # and the Flask application instance + # + # mw1 mw2 mw3 Flask app + # o ---- o ---- o ---- o + # / + # o Flask-SocketIO + # \ middleware + # o + # Flask-SocketIO WebSocket handler + # + # BECOMES + # + # dbg-mw mw1 mw2 mw3 Flask app + # o ---- o ---- o ---- o ---- o + # / + # o Flask-SocketIO + # \ middleware + # o + # Flask-SocketIO WebSocket handler + # + self.sockio_mw.wsgi_app = DebuggedApplication( + self.sockio_mw.wsgi_app, evalex=True) + + allow_unsafe_werkzeug = kwargs.pop('allow_unsafe_werkzeug', False) + if self.server.eio.async_mode == 'threading': + try: + import simple_websocket # noqa: F401 + except ImportError: + from werkzeug._internal import _log + _log('warning', 'WebSocket transport not available. Install ' + 'simple-websocket for improved performance.') + if not sys.stdin or not sys.stdin.isatty(): # pragma: no cover + if not allow_unsafe_werkzeug: + raise RuntimeError('The Werkzeug web server is not ' + 'designed to run in production. Pass ' + 'allow_unsafe_werkzeug=True to the ' + 'run() method to disable this error.') + else: + from werkzeug._internal import _log + _log('warning', ('Werkzeug appears to be used in a ' + 'production deployment. Consider ' + 'switching to a production web server ' + 'instead.')) + app.run(host=host, port=port, threaded=True, + use_reloader=use_reloader, **reloader_options, **kwargs) + elif self.server.eio.async_mode == 'eventlet': + def run_server(): + import eventlet + import eventlet.wsgi + import eventlet.green + addresses = eventlet.green.socket.getaddrinfo(host, port) + if not addresses: + raise RuntimeError( + 'Could not resolve host to a valid address') + eventlet_socket = eventlet.listen(addresses[0][4], + addresses[0][0]) + + # If provided an SSL argument, use an SSL socket + ssl_args = ['keyfile', 'certfile', 'server_side', 'cert_reqs', + 'ssl_version', 'ca_certs', + 'do_handshake_on_connect', 'suppress_ragged_eofs', + 'ciphers'] + ssl_params = {k: kwargs[k] for k in kwargs + if k in ssl_args and kwargs[k] is not None} + for k in ssl_args: + kwargs.pop(k, None) + if len(ssl_params) > 0: + ssl_params['server_side'] = True # Listening requires true + eventlet_socket = eventlet.wrap_ssl(eventlet_socket, + **ssl_params) + + eventlet.wsgi.server(eventlet_socket, app, + log_output=log_output, **kwargs) + + if use_reloader: + run_with_reloader(run_server, **reloader_options) + else: + run_server() + elif self.server.eio.async_mode == 'gevent': + from gevent import pywsgi + try: + from geventwebsocket.handler import WebSocketHandler + gevent_websocket = True + except ImportError: + # WebSocket support will come from the simple-websocket package + gevent_websocket = False + + log = 'default' + if not log_output: + log = None + if gevent_websocket: + self.wsgi_server = pywsgi.WSGIServer( + (host, port), app, handler_class=WebSocketHandler, + log=log, **kwargs) + else: + self.wsgi_server = pywsgi.WSGIServer((host, port), app, + log=log, **kwargs) + + if use_reloader: + # monkey patching is required by the reloader + from gevent import monkey + monkey.patch_thread() + monkey.patch_time() + + def run_server(): + self.wsgi_server.serve_forever() + + run_with_reloader(run_server, **reloader_options) + else: + self.wsgi_server.serve_forever() + + def stop(self): + """Stop a running SocketIO web server. + + This method must be called from a HTTP or SocketIO handler function. + """ + if self.server.eio.async_mode == 'threading': + func = flask.request.environ.get('werkzeug.server.shutdown') + if func: + func() + else: + raise RuntimeError('Cannot stop unknown web server') + elif self.server.eio.async_mode == 'eventlet': + raise SystemExit + elif self.server.eio.async_mode == 'gevent': + self.wsgi_server.stop() + + def start_background_task(self, target, *args, **kwargs): + """Start a background task using the appropriate async model. + + This is a utility function that applications can use to start a + background task using the method that is compatible with the + selected async mode. + + :param target: the target function to execute. + :param args: arguments to pass to the function. + :param kwargs: keyword arguments to pass to the function. + + This function returns an object that represents the background task, + on which the ``join()`` method can be invoked to wait for the task to + complete. + """ + return self.server.start_background_task(target, *args, **kwargs) + + def sleep(self, seconds=0): + """Sleep for the requested amount of time using the appropriate async + model. + + This is a utility function that applications can use to put a task to + sleep without having to worry about using the correct call for the + selected async mode. + """ + return self.server.sleep(seconds) + + def test_client(self, app, namespace=None, query_string=None, + headers=None, auth=None, flask_test_client=None): + """The Socket.IO test client is useful for testing a Flask-SocketIO + server. It works in a similar way to the Flask Test Client, but + adapted to the Socket.IO server. + + :param app: The Flask application instance. + :param namespace: The namespace for the client. If not provided, the + client connects to the server on the global + namespace. + :param query_string: A string with custom query string arguments. + :param headers: A dictionary with custom HTTP headers. + :param auth: Optional authentication data, given as a dictionary. + :param flask_test_client: The instance of the Flask test client + currently in use. Passing the Flask test + client is optional, but is necessary if you + want the Flask user session and any other + cookies set in HTTP routes accessible from + Socket.IO events. + """ + return SocketIOTestClient(app, self, namespace=namespace, + query_string=query_string, headers=headers, + auth=auth, + flask_test_client=flask_test_client) + + def _handle_event(self, handler, message, namespace, sid, *args): + environ = self.server.get_environ(sid, namespace=namespace) + if not environ: + # we don't have record of this client, ignore this event + return '', 400 + app = environ['flask.app'] + with app.request_context(environ): + if self.manage_session: + # manage a separate session for this client's Socket.IO events + # created as a copy of the regular user session + if 'saved_session' not in environ: + environ['saved_session'] = _ManagedSession(flask.session) + session_obj = environ['saved_session'] + if hasattr(flask, 'globals') and \ + hasattr(flask.globals, 'request_ctx'): + # update session for Flask >= 2.2 + ctx = flask.globals.request_ctx._get_current_object() + else: # pragma: no cover + # update session for Flask < 2.2 + ctx = flask._request_ctx_stack.top + ctx.session = session_obj + else: + # let Flask handle the user session + # for cookie based sessions, this effectively freezes the + # session to its state at connection time + # for server-side sessions, this allows HTTP and Socket.IO to + # share the session, with both having read/write access to it + session_obj = flask.session._get_current_object() + flask.request.sid = sid + flask.request.namespace = namespace + flask.request.event = {'message': message, 'args': args} + try: + if message == 'connect': + auth = args[1] if len(args) > 1 else None + try: + ret = handler(auth) + except TypeError: + ret = handler() + else: + ret = handler(*args) + except ConnectionRefusedError: + raise # let this error bubble up to python-socketio + except: + err_handler = self.exception_handlers.get( + namespace, self.default_exception_handler) + if err_handler is None: + raise + type, value, traceback = sys.exc_info() + return err_handler(value) + if not self.manage_session: + # when Flask is managing the user session, it needs to save it + if not hasattr(session_obj, 'modified') or \ + session_obj.modified: + resp = app.response_class() + app.session_interface.save_session(app, session_obj, resp) + return ret + + +def emit(event, *args, **kwargs): + """Emit a SocketIO event. + + This function emits a SocketIO event to one or more connected clients. A + JSON blob can be attached to the event as payload. This is a function that + can only be called from a SocketIO event handler, as in obtains some + information from the current client context. Example:: + + @socketio.on('my event') + def handle_my_custom_event(json): + emit('my response', {'data': 42}) + + :param event: The name of the user event to emit. + :param args: A dictionary with the JSON data to send as payload. + :param namespace: The namespace under which the message is to be sent. + Defaults to the namespace used by the originating event. + A ``'/'`` can be used to explicitly specify the global + namespace. + :param callback: Callback function to invoke with the client's + acknowledgement. + :param broadcast: ``True`` to send the message to all clients, or ``False`` + to only reply to the sender of the originating event. + :param to: Send the message to all the users in the given room, or to the + user with the given session ID. If this argument is not set and + ``broadcast`` is ``False``, then the message is sent only to the + originating user. + :param include_self: ``True`` to include the sender when broadcasting or + addressing a room, or ``False`` to send to everyone + but the sender. + :param skip_sid: The session id of a client to ignore when broadcasting + or addressing a room. This is typically set to the + originator of the message, so that everyone except + that client receive the message. To skip multiple sids + pass a list. + :param ignore_queue: Only used when a message queue is configured. If + set to ``True``, the event is emitted to the + clients directly, without going through the queue. + This is more efficient, but only works when a + single server process is used, or when there is a + single addressee. It is recommended to always leave + this parameter with its default value of ``False``. + """ + if 'namespace' in kwargs: + namespace = kwargs['namespace'] + else: + namespace = flask.request.namespace + callback = kwargs.get('callback') + broadcast = kwargs.get('broadcast') + to = kwargs.pop('to', None) or kwargs.pop('room', None) + if to is None and not broadcast: + to = flask.request.sid + include_self = kwargs.get('include_self', True) + skip_sid = kwargs.get('skip_sid') + ignore_queue = kwargs.get('ignore_queue', False) + + socketio = flask.current_app.extensions['socketio'] + return socketio.emit(event, *args, namespace=namespace, to=to, + include_self=include_self, skip_sid=skip_sid, + callback=callback, ignore_queue=ignore_queue) + + +def call(event, *args, **kwargs): # pragma: no cover + """Emit a SocketIO event and wait for the response. + + This function issues an emit with a callback and waits for the callback to + be invoked by the client before returning. If the callback isn’t invoked + before the timeout, then a TimeoutError exception is raised. If the + Socket.IO connection drops during the wait, this method still waits until + the specified timeout. Example:: + + def get_status(client, data): + status = call('status', {'data': data}, to=client) + + :param event: The name of the user event to emit. + :param args: A dictionary with the JSON data to send as payload. + :param namespace: The namespace under which the message is to be sent. + Defaults to the namespace used by the originating event. + A ``'/'`` can be used to explicitly specify the global + namespace. + :param to: The session ID of the recipient client. If this argument is not + given, the event is sent to the originating client. + :param timeout: The waiting timeout. If the timeout is reached before the + client acknowledges the event, then a ``TimeoutError`` + exception is raised. The default is 60 seconds. + :param ignore_queue: Only used when a message queue is configured. If + set to ``True``, the event is emitted to the + client directly, without going through the queue. + This is more efficient, but only works when a + single server process is used, or when there is a + single addressee. It is recommended to always leave + this parameter with its default value of ``False``. + """ + if 'namespace' in kwargs: + namespace = kwargs['namespace'] + else: + namespace = flask.request.namespace + to = kwargs.pop('to', None) or kwargs.pop('room', None) + if to is None: + to = flask.request.sid + timeout = kwargs.get('timeout', 60) + ignore_queue = kwargs.get('ignore_queue', False) + + socketio = flask.current_app.extensions['socketio'] + return socketio.call(event, *args, namespace=namespace, to=to, + ignore_queue=ignore_queue, timeout=timeout) + + +def send(message, **kwargs): + """Send a SocketIO message. + + This function sends a simple SocketIO message to one or more connected + clients. The message can be a string or a JSON blob. This is a simpler + version of ``emit()``, which should be preferred. This is a function that + can only be called from a SocketIO event handler. + + :param message: The message to send, either a string or a JSON blob. + :param json: ``True`` if ``message`` is a JSON blob, ``False`` + otherwise. + :param namespace: The namespace under which the message is to be sent. + Defaults to the namespace used by the originating event. + An empty string can be used to use the global namespace. + :param callback: Callback function to invoke with the client's + acknowledgement. + :param broadcast: ``True`` to send the message to all connected clients, or + ``False`` to only reply to the sender of the originating + event. + :param to: Send the message to all the users in the given room, or to the + user with the given session ID. If this argument is not set and + ``broadcast`` is ``False``, then the message is sent only to the + originating user. + :param include_self: ``True`` to include the sender when broadcasting or + addressing a room, or ``False`` to send to everyone + but the sender. + :param skip_sid: The session id of a client to ignore when broadcasting + or addressing a room. This is typically set to the + originator of the message, so that everyone except + that client receive the message. To skip multiple sids + pass a list. + :param ignore_queue: Only used when a message queue is configured. If + set to ``True``, the event is emitted to the + clients directly, without going through the queue. + This is more efficient, but only works when a + single server process is used, or when there is a + single addressee. It is recommended to always leave + this parameter with its default value of ``False``. + """ + json = kwargs.get('json', False) + if 'namespace' in kwargs: + namespace = kwargs['namespace'] + else: + namespace = flask.request.namespace + callback = kwargs.get('callback') + broadcast = kwargs.get('broadcast') + to = kwargs.pop('to', None) or kwargs.pop('room', None) + if to is None and not broadcast: + to = flask.request.sid + include_self = kwargs.get('include_self', True) + skip_sid = kwargs.get('skip_sid') + ignore_queue = kwargs.get('ignore_queue', False) + + socketio = flask.current_app.extensions['socketio'] + return socketio.send(message, json=json, namespace=namespace, to=to, + include_self=include_self, skip_sid=skip_sid, + callback=callback, ignore_queue=ignore_queue) + + +def join_room(room, sid=None, namespace=None): + """Join a room. + + This function puts the user in a room, under the current namespace. The + user and the namespace are obtained from the event context. This is a + function that can only be called from a SocketIO event handler. Example:: + + @socketio.on('join') + def on_join(data): + username = session['username'] + room = data['room'] + join_room(room) + send(username + ' has entered the room.', to=room) + + :param room: The name of the room to join. + :param sid: The session id of the client. If not provided, the client is + obtained from the request context. + :param namespace: The namespace for the room. If not provided, the + namespace is obtained from the request context. + """ + socketio = flask.current_app.extensions['socketio'] + sid = sid or flask.request.sid + namespace = namespace or flask.request.namespace + socketio.server.enter_room(sid, room, namespace=namespace) + + +def leave_room(room, sid=None, namespace=None): + """Leave a room. + + This function removes the user from a room, under the current namespace. + The user and the namespace are obtained from the event context. Example:: + + @socketio.on('leave') + def on_leave(data): + username = session['username'] + room = data['room'] + leave_room(room) + send(username + ' has left the room.', to=room) + + :param room: The name of the room to leave. + :param sid: The session id of the client. If not provided, the client is + obtained from the request context. + :param namespace: The namespace for the room. If not provided, the + namespace is obtained from the request context. + """ + socketio = flask.current_app.extensions['socketio'] + sid = sid or flask.request.sid + namespace = namespace or flask.request.namespace + socketio.server.leave_room(sid, room, namespace=namespace) + + +def close_room(room, namespace=None): + """Close a room. + + This function removes any users that are in the given room and then deletes + the room from the server. + + :param room: The name of the room to close. + :param namespace: The namespace for the room. If not provided, the + namespace is obtained from the request context. + """ + socketio = flask.current_app.extensions['socketio'] + namespace = namespace or flask.request.namespace + socketio.server.close_room(room, namespace=namespace) + + +def rooms(sid=None, namespace=None): + """Return a list of the rooms the client is in. + + This function returns all the rooms the client has entered, including its + own room, assigned by the Socket.IO server. + + :param sid: The session id of the client. If not provided, the client is + obtained from the request context. + :param namespace: The namespace for the room. If not provided, the + namespace is obtained from the request context. + """ + socketio = flask.current_app.extensions['socketio'] + sid = sid or flask.request.sid + namespace = namespace or flask.request.namespace + return socketio.server.rooms(sid, namespace=namespace) + + +def disconnect(sid=None, namespace=None, silent=False): + """Disconnect the client. + + This function terminates the connection with the client. As a result of + this call the client will receive a disconnect event. Example:: + + @socketio.on('message') + def receive_message(msg): + if is_banned(session['username']): + disconnect() + else: + # ... + + :param sid: The session id of the client. If not provided, the client is + obtained from the request context. + :param namespace: The namespace for the room. If not provided, the + namespace is obtained from the request context. + :param silent: this option is deprecated. + """ + socketio = flask.current_app.extensions['socketio'] + sid = sid or flask.request.sid + namespace = namespace or flask.request.namespace + return socketio.server.disconnect(sid, namespace=namespace) diff --git a/tapdown/lib/python3.11/site-packages/flask_socketio/namespace.py b/tapdown/lib/python3.11/site-packages/flask_socketio/namespace.py new file mode 100644 index 0000000..541fa79 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_socketio/namespace.py @@ -0,0 +1,54 @@ +from socketio import Namespace as _Namespace + + +class Namespace(_Namespace): + def __init__(self, namespace=None): + super().__init__(namespace) + self.socketio = None + + def _set_socketio(self, socketio): + self.socketio = socketio + + def trigger_event(self, event, *args): + """Dispatch an event to the proper handler method. + + In the most common usage, this method is not overloaded by subclasses, + as it performs the routing of events to methods. However, this + method can be overridden if special dispatching rules are needed, or if + having a single method that catches all events is desired. + """ + handler_name = 'on_' + (event or '') + if not hasattr(self, handler_name): + # there is no handler for this event, so we ignore it + return + handler = getattr(self, handler_name) + try: + return self.socketio._handle_event(handler, event, self.namespace, + *args) + except TypeError: + if event == 'disconnect': + # legacy disconnect events do not have the reason argument + return self.socketio._handle_event( + handler, event, self.namespace, *args[:-1]) + else: + raise + + def emit(self, event, data=None, room=None, include_self=True, + namespace=None, callback=None): + """Emit a custom event to one or more connected clients.""" + return self.socketio.emit(event, data, room=room, + include_self=include_self, + namespace=namespace or self.namespace, + callback=callback) + + def send(self, data, room=None, include_self=True, namespace=None, + callback=None): + """Send a message to one or more connected clients.""" + return self.socketio.send(data, room=room, include_self=include_self, + namespace=namespace or self.namespace, + callback=callback) + + def close_room(self, room, namespace=None): + """Close a room.""" + return self.socketio.close_room(room=room, + namespace=namespace or self.namespace) diff --git a/tapdown/lib/python3.11/site-packages/flask_socketio/test_client.py b/tapdown/lib/python3.11/site-packages/flask_socketio/test_client.py new file mode 100644 index 0000000..312bac1 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_socketio/test_client.py @@ -0,0 +1,236 @@ +import uuid + +from socketio import packet +from socketio.pubsub_manager import PubSubManager +from werkzeug.test import EnvironBuilder + + +class SocketIOTestClient: + """ + This class is useful for testing a Flask-SocketIO server. It works in a + similar way to the Flask Test Client, but adapted to the Socket.IO server. + + :param app: The Flask application instance. + :param socketio: The application's ``SocketIO`` instance. + :param namespace: The namespace for the client. If not provided, the client + connects to the server on the global namespace. + :param query_string: A string with custom query string arguments. + :param headers: A dictionary with custom HTTP headers. + :param auth: Optional authentication data, given as a dictionary. + :param flask_test_client: The instance of the Flask test client + currently in use. Passing the Flask test + client is optional, but is necessary if you + want the Flask user session and any other + cookies set in HTTP routes accessible from + Socket.IO events. + """ + clients = {} + + def __init__(self, app, socketio, namespace=None, query_string=None, + headers=None, auth=None, flask_test_client=None): + def _mock_send_packet(eio_sid, pkt): + # make sure the packet can be encoded and decoded + epkt = pkt.encode() + if not isinstance(epkt, list): + pkt = packet.Packet(encoded_packet=epkt) + else: + pkt = packet.Packet(encoded_packet=epkt[0]) + for att in epkt[1:]: + pkt.add_attachment(att) + client = self.clients.get(eio_sid) + if not client: + return + if pkt.packet_type == packet.EVENT or \ + pkt.packet_type == packet.BINARY_EVENT: + if pkt.data[0] == 'message' or pkt.data[0] == 'json': + client.queue.append({ + 'name': pkt.data[0], + 'args': pkt.data[1], + 'namespace': pkt.namespace or '/'}) + else: + client.queue.append({ + 'name': pkt.data[0], + 'args': pkt.data[1:], + 'namespace': pkt.namespace or '/'}) + elif pkt.packet_type == packet.ACK or \ + pkt.packet_type == packet.BINARY_ACK: + client.acks = {'args': pkt.data, + 'namespace': pkt.namespace or '/'} + elif pkt.packet_type in [packet.DISCONNECT, packet.CONNECT_ERROR]: + client.connected[pkt.namespace or '/'] = False + + _current_packet = None + + def _mock_send_eio_packet(eio_sid, eio_pkt): + nonlocal _current_packet + if _current_packet is not None: + _current_packet.add_attachment(eio_pkt.data) + if _current_packet.attachment_count == \ + len(_current_packet.attachments): + _mock_send_packet(eio_sid, _current_packet) + _current_packet = None + else: + pkt = packet.Packet(encoded_packet=eio_pkt.data) + if pkt.attachment_count == 0: + _mock_send_packet(eio_sid, pkt) + else: + _current_packet = pkt + + self.app = app + self.flask_test_client = flask_test_client + self.eio_sid = uuid.uuid4().hex + self.clients[self.eio_sid] = self + self.callback_counter = 0 + self.socketio = socketio + self.connected = {} + self.queue = [] + self.acks = None + socketio.server._send_packet = _mock_send_packet + socketio.server._send_eio_packet = _mock_send_eio_packet + socketio.server.environ[self.eio_sid] = {} + socketio.server.async_handlers = False # easier to test when + socketio.server.eio.async_handlers = False # events are sync + if isinstance(socketio.server.manager, PubSubManager): + raise RuntimeError('Test client cannot be used with a message ' + 'queue. Disable the queue on your test ' + 'configuration.') + socketio.server.manager.initialize() + self.connect(namespace=namespace, query_string=query_string, + headers=headers, auth=auth) + + def is_connected(self, namespace=None): + """Check if a namespace is connected. + + :param namespace: The namespace to check. The global namespace is + assumed if this argument is not provided. + """ + return self.connected.get(namespace or '/', False) + + def connect(self, namespace=None, query_string=None, headers=None, + auth=None): + """Connect the client. + + :param namespace: The namespace for the client. If not provided, the + client connects to the server on the global + namespace. + :param query_string: A string with custom query string arguments. + :param headers: A dictionary with custom HTTP headers. + :param auth: Optional authentication data, given as a dictionary. + + Note that it is usually not necessary to explicitly call this method, + since a connection is automatically established when an instance of + this class is created. An example where it this method would be useful + is when the application accepts multiple namespace connections. + """ + url = '/socket.io' + namespace = namespace or '/' + if query_string: + if query_string[0] != '?': + query_string = '?' + query_string + url += query_string + environ = EnvironBuilder(url, headers=headers).get_environ() + environ['flask.app'] = self.app + if self.flask_test_client: + # inject cookies from Flask + if hasattr(self.flask_test_client, '_add_cookies_to_wsgi'): + # flask >= 2.3 + self.flask_test_client._add_cookies_to_wsgi(environ) + else: # pragma: no cover + # flask < 2.3 + self.flask_test_client.cookie_jar.inject_wsgi(environ) + self.socketio.server._handle_eio_connect(self.eio_sid, environ) + pkt = packet.Packet(packet.CONNECT, auth, namespace=namespace) + self.socketio.server._handle_eio_message(self.eio_sid, pkt.encode()) + sid = self.socketio.server.manager.sid_from_eio_sid(self.eio_sid, + namespace) + if sid: + self.connected[namespace] = True + + def disconnect(self, namespace=None): + """Disconnect the client. + + :param namespace: The namespace to disconnect. The global namespace is + assumed if this argument is not provided. + """ + if not self.is_connected(namespace): + raise RuntimeError('not connected') + pkt = packet.Packet(packet.DISCONNECT, namespace=namespace) + self.socketio.server._handle_eio_message(self.eio_sid, pkt.encode()) + del self.connected[namespace or '/'] + + def emit(self, event, *args, **kwargs): + """Emit an event to the server. + + :param event: The event name. + :param *args: The event arguments. + :param callback: ``True`` if the client requests a callback, ``False`` + if not. Note that client-side callbacks are not + implemented, a callback request will just tell the + server to provide the arguments to invoke the + callback, but no callback is invoked. Instead, the + arguments that the server provided for the callback + are returned by this function. + :param namespace: The namespace of the event. The global namespace is + assumed if this argument is not provided. + """ + namespace = kwargs.pop('namespace', None) + if not self.is_connected(namespace): + raise RuntimeError('not connected') + callback = kwargs.pop('callback', False) + id = None + if callback: + self.callback_counter += 1 + id = self.callback_counter + pkt = packet.Packet(packet.EVENT, data=[event] + list(args), + namespace=namespace, id=id) + encoded_pkt = pkt.encode() + if isinstance(encoded_pkt, list): + for epkt in encoded_pkt: + self.socketio.server._handle_eio_message(self.eio_sid, epkt) + else: + self.socketio.server._handle_eio_message(self.eio_sid, encoded_pkt) + if self.acks is not None: + ack = self.acks + self.acks = None + return ack['args'][0] if len(ack['args']) == 1 \ + else ack['args'] + + def send(self, data, json=False, callback=False, namespace=None): + """Send a text or JSON message to the server. + + :param data: A string, dictionary or list to send to the server. + :param json: ``True`` to send a JSON message, ``False`` to send a text + message. + :param callback: ``True`` if the client requests a callback, ``False`` + if not. Note that client-side callbacks are not + implemented, a callback request will just tell the + server to provide the arguments to invoke the + callback, but no callback is invoked. Instead, the + arguments that the server provided for the callback + are returned by this function. + :param namespace: The namespace of the event. The global namespace is + assumed if this argument is not provided. + """ + if json: + msg = 'json' + else: + msg = 'message' + return self.emit(msg, data, callback=callback, namespace=namespace) + + def get_received(self, namespace=None): + """Return the list of messages received from the server. + + Since this is not a real client, any time the server emits an event, + the event is simply stored. The test code can invoke this method to + obtain the list of events that were received since the last call. + + :param namespace: The namespace to get events from. The global + namespace is assumed if this argument is not + provided. + """ + if not self.is_connected(namespace): + raise RuntimeError('not connected') + namespace = namespace or '/' + r = [pkt for pkt in self.queue if pkt['namespace'] == namespace] + self.queue = [pkt for pkt in self.queue if pkt not in r] + return r diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/LICENSE.rst b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/LICENSE.rst new file mode 100644 index 0000000..9d227a0 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/LICENSE.rst @@ -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/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/METADATA new file mode 100644 index 0000000..92f239c --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/METADATA @@ -0,0 +1,109 @@ +Metadata-Version: 2.1 +Name: Flask-SQLAlchemy +Version: 3.1.1 +Summary: Add SQLAlchemy support to your Flask application. +Maintainer-email: Pallets +Requires-Python: >=3.8 +Description-Content-Type: text/x-rst +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 +Requires-Dist: flask>=2.2.5 +Requires-Dist: sqlalchemy>=2.0.16 +Project-URL: Changes, https://flask-sqlalchemy.palletsprojects.com/changes/ +Project-URL: Chat, https://discord.gg/pallets +Project-URL: Documentation, https://flask-sqlalchemy.palletsprojects.com +Project-URL: Donate, https://palletsprojects.com/donate +Project-URL: Issue Tracker, https://github.com/pallets-eco/flask-sqlalchemy/issues/ +Project-URL: Source Code, https://github.com/pallets-eco/flask-sqlalchemy/ + +Flask-SQLAlchemy +================ + +Flask-SQLAlchemy is an extension for `Flask`_ that adds support for +`SQLAlchemy`_ to your application. It aims to simplify using SQLAlchemy +with Flask by providing useful defaults and extra helpers that make it +easier to accomplish common tasks. + +.. _Flask: https://palletsprojects.com/p/flask/ +.. _SQLAlchemy: https://www.sqlalchemy.org + + +Installing +---------- + +Install and update using `pip`_: + +.. code-block:: text + + $ pip install -U Flask-SQLAlchemy + +.. _pip: https://pip.pypa.io/en/stable/getting-started/ + + +A Simple Example +---------------- + +.. code-block:: python + + from flask import Flask + from flask_sqlalchemy import SQLAlchemy + from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column + + app = Flask(__name__) + app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///example.sqlite" + + class Base(DeclarativeBase): + pass + + db = SQLAlchemy(app, model_class=Base) + + class User(db.Model): + id: Mapped[int] = mapped_column(db.Integer, primary_key=True) + username: Mapped[str] = mapped_column(db.String, unique=True, nullable=False) + + with app.app_context(): + db.create_all() + + db.session.add(User(username="example")) + db.session.commit() + + users = db.session.execute(db.select(User)).scalars() + + +Contributing +------------ + +For guidance on setting up a development environment and how to make a +contribution to Flask-SQLAlchemy, see the `contributing guidelines`_. + +.. _contributing guidelines: https://github.com/pallets-eco/flask-sqlalchemy/blob/main/CONTRIBUTING.rst + + +Donate +------ + +The Pallets organization develops and supports Flask-SQLAlchemy and +other popular packages. In order to grow the community of contributors +and users, and allow the maintainers to devote more time to the +projects, `please donate today`_. + +.. _please donate today: https://palletsprojects.com/donate + + +Links +----- + +- Documentation: https://flask-sqlalchemy.palletsprojects.com/ +- Changes: https://flask-sqlalchemy.palletsprojects.com/changes/ +- PyPI Releases: https://pypi.org/project/Flask-SQLAlchemy/ +- Source Code: https://github.com/pallets-eco/flask-sqlalchemy/ +- Issue Tracker: https://github.com/pallets-eco/flask-sqlalchemy/issues/ +- Website: https://palletsprojects.com/ +- Twitter: https://twitter.com/PalletsTeam +- Chat: https://discord.gg/pallets + diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/RECORD new file mode 100644 index 0000000..4df6c83 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/RECORD @@ -0,0 +1,27 @@ +flask_sqlalchemy-3.1.1.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +flask_sqlalchemy-3.1.1.dist-info/LICENSE.rst,sha256=SJqOEQhQntmKN7uYPhHg9-HTHwvY-Zp5yESOf_N9B-o,1475 +flask_sqlalchemy-3.1.1.dist-info/METADATA,sha256=lBxR1akBt7n9XBjIVTL2OV52OhCfFrb-Mqtoe0DCbR8,3432 +flask_sqlalchemy-3.1.1.dist-info/RECORD,, +flask_sqlalchemy-3.1.1.dist-info/REQUESTED,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask_sqlalchemy-3.1.1.dist-info/WHEEL,sha256=EZbGkh7Ie4PoZfRQ8I0ZuP9VklN_TvcZ6DSE5Uar4z4,81 +flask_sqlalchemy/__init__.py,sha256=he_w4qQQVS2Z1ms5GCTptDTXNOXBXw0n8zSuWCp8n6Y,653 +flask_sqlalchemy/__pycache__/__init__.cpython-311.pyc,, +flask_sqlalchemy/__pycache__/cli.cpython-311.pyc,, +flask_sqlalchemy/__pycache__/extension.cpython-311.pyc,, +flask_sqlalchemy/__pycache__/model.cpython-311.pyc,, +flask_sqlalchemy/__pycache__/pagination.cpython-311.pyc,, +flask_sqlalchemy/__pycache__/query.cpython-311.pyc,, +flask_sqlalchemy/__pycache__/record_queries.cpython-311.pyc,, +flask_sqlalchemy/__pycache__/session.cpython-311.pyc,, +flask_sqlalchemy/__pycache__/table.cpython-311.pyc,, +flask_sqlalchemy/__pycache__/track_modifications.cpython-311.pyc,, +flask_sqlalchemy/cli.py,sha256=pg3QDxP36GW2qnwe_CpPtkRhPchyVSGM6zlBNWuNCFE,484 +flask_sqlalchemy/extension.py,sha256=71tP_kNtb5VgZdafy_OH1sWdZOA6PaT7cJqX7tKgZ-k,38261 +flask_sqlalchemy/model.py,sha256=_mSisC2Eni0TgTyFWeN_O4LIexTeP_sVTdxh03yMK50,11461 +flask_sqlalchemy/pagination.py,sha256=JFpllrqkRkwacb8DAmQWaz9wsvQa0dypfSkhUDSC2ws,11119 +flask_sqlalchemy/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0 +flask_sqlalchemy/query.py,sha256=Uls9qbmnpb9Vba43EDfsRP17eHJ0X4VG7SE22tH5R3g,3748 +flask_sqlalchemy/record_queries.py,sha256=ouS1ayj16h76LJprx13iYdoFZbm6m8OncrOgAVbG1Sk,3520 +flask_sqlalchemy/session.py,sha256=pBbtN8iDc8yuGVt0k18BvZHh2uEI7QPzZXO7eXrRi1g,3426 +flask_sqlalchemy/table.py,sha256=wAPOy8qwyAxpMwOIUJY4iMOultzz2W0D6xvBkQ7U2CE,859 +flask_sqlalchemy/track_modifications.py,sha256=yieyozj7IiVzwnAGZ-ZrgqrzjrUfG0kPrXBfW_hStSU,2755 diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/REQUESTED b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.dist-info/WHEEL new file mode 100644 index 0000000..3b5e64b --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy-3.1.1.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/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/__init__.py b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/__init__.py new file mode 100644 index 0000000..c2fa059 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/__init__.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +import typing as t + +from .extension import SQLAlchemy + +__all__ = [ + "SQLAlchemy", +] + + +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-SQLAlchemy 3.2. Use feature detection or" + " 'importlib.metadata.version(\"flask-sqlalchemy\")' instead.", + DeprecationWarning, + stacklevel=2, + ) + return importlib.metadata.version("flask-sqlalchemy") + + raise AttributeError(name) diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/cli.py b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/cli.py new file mode 100644 index 0000000..d7d7e4b --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/cli.py @@ -0,0 +1,16 @@ +from __future__ import annotations + +import typing as t + +from flask import current_app + + +def add_models_to_shell() -> dict[str, t.Any]: + """Registered with :meth:`~flask.Flask.shell_context_processor` if + ``add_models_to_shell`` is enabled. Adds the ``db`` instance and all model classes + to ``flask shell``. + """ + db = current_app.extensions["sqlalchemy"] + out = {m.class_.__name__: m.class_ for m in db.Model._sa_registry.mappers} + out["db"] = db + return out diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/extension.py b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/extension.py new file mode 100644 index 0000000..43e1b9a --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/extension.py @@ -0,0 +1,1008 @@ +from __future__ import annotations + +import os +import types +import typing as t +import warnings +from weakref import WeakKeyDictionary + +import sqlalchemy as sa +import sqlalchemy.event as sa_event +import sqlalchemy.exc as sa_exc +import sqlalchemy.orm as sa_orm +from flask import abort +from flask import current_app +from flask import Flask +from flask import has_app_context + +from .model import _QueryProperty +from .model import BindMixin +from .model import DefaultMeta +from .model import DefaultMetaNoName +from .model import Model +from .model import NameMixin +from .pagination import Pagination +from .pagination import SelectPagination +from .query import Query +from .session import _app_ctx_id +from .session import Session +from .table import _Table + +_O = t.TypeVar("_O", bound=object) # Based on sqlalchemy.orm._typing.py + + +# Type accepted for model_class argument +_FSA_MCT = t.TypeVar( + "_FSA_MCT", + bound=t.Union[ + t.Type[Model], + sa_orm.DeclarativeMeta, + t.Type[sa_orm.DeclarativeBase], + t.Type[sa_orm.DeclarativeBaseNoMeta], + ], +) + + +# Type returned by make_declarative_base +class _FSAModel(Model): + metadata: sa.MetaData + + +def _get_2x_declarative_bases( + model_class: _FSA_MCT, +) -> list[t.Type[t.Union[sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta]]]: + return [ + b + for b in model_class.__bases__ + if issubclass(b, (sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta)) + ] + + +class SQLAlchemy: + """Integrates SQLAlchemy with Flask. This handles setting up one or more engines, + associating tables and models with specific engines, and cleaning up connections and + sessions after each request. + + Only the engine configuration is specific to each application, other things like + the model, table, metadata, and session are shared for all applications using that + extension instance. Call :meth:`init_app` to configure the extension on an + application. + + After creating the extension, create model classes by subclassing :attr:`Model`, and + table classes with :attr:`Table`. These can be accessed before :meth:`init_app` is + called, making it possible to define the models separately from the application. + + Accessing :attr:`session` and :attr:`engine` requires an active Flask application + context. This includes methods like :meth:`create_all` which use the engine. + + This class also provides access to names in SQLAlchemy's ``sqlalchemy`` and + ``sqlalchemy.orm`` modules. For example, you can use ``db.Column`` and + ``db.relationship`` instead of importing ``sqlalchemy.Column`` and + ``sqlalchemy.orm.relationship``. This can be convenient when defining models. + + :param app: Call :meth:`init_app` on this Flask application now. + :param metadata: Use this as the default :class:`sqlalchemy.schema.MetaData`. Useful + for setting a naming convention. + :param session_options: Arguments used by :attr:`session` to create each session + instance. A ``scopefunc`` key will be passed to the scoped session, not the + session instance. See :class:`sqlalchemy.orm.sessionmaker` for a list of + arguments. + :param query_class: Use this as the default query class for models and dynamic + relationships. The query interface is considered legacy in SQLAlchemy. + :param model_class: Use this as the model base class when creating the declarative + model class :attr:`Model`. Can also be a fully created declarative model class + for further customization. + :param engine_options: Default arguments used when creating every engine. These are + lower precedence than application config. See :func:`sqlalchemy.create_engine` + for a list of arguments. + :param add_models_to_shell: Add the ``db`` instance and all model classes to + ``flask shell``. + + .. versionchanged:: 3.1.0 + The ``metadata`` parameter can still be used with SQLAlchemy 1.x classes, + but is ignored when using SQLAlchemy 2.x style of declarative classes. + Instead, specify metadata on your Base class. + + .. versionchanged:: 3.1.0 + Added the ``disable_autonaming`` parameter. + + .. versionchanged:: 3.1.0 + Changed ``model_class`` parameter to accepta SQLAlchemy 2.x + declarative base subclass. + + .. versionchanged:: 3.0 + An active Flask application context is always required to access ``session`` and + ``engine``. + + .. versionchanged:: 3.0 + Separate ``metadata`` are used for each bind key. + + .. versionchanged:: 3.0 + The ``engine_options`` parameter is applied as defaults before per-engine + configuration. + + .. versionchanged:: 3.0 + The session class can be customized in ``session_options``. + + .. versionchanged:: 3.0 + Added the ``add_models_to_shell`` parameter. + + .. versionchanged:: 3.0 + Engines are created when calling ``init_app`` rather than the first time they + are accessed. + + .. versionchanged:: 3.0 + All parameters except ``app`` are keyword-only. + + .. versionchanged:: 3.0 + The extension instance is stored directly as ``app.extensions["sqlalchemy"]``. + + .. versionchanged:: 3.0 + Setup methods are renamed with a leading underscore. They are considered + internal interfaces which may change at any time. + + .. versionchanged:: 3.0 + Removed the ``use_native_unicode`` parameter and config. + + .. versionchanged:: 2.4 + Added the ``engine_options`` parameter. + + .. versionchanged:: 2.1 + Added the ``metadata``, ``query_class``, and ``model_class`` parameters. + + .. versionchanged:: 2.1 + Use the same query class across ``session``, ``Model.query`` and + ``Query``. + + .. versionchanged:: 0.16 + ``scopefunc`` is accepted in ``session_options``. + + .. versionchanged:: 0.10 + Added the ``session_options`` parameter. + """ + + def __init__( + self, + app: Flask | None = None, + *, + metadata: sa.MetaData | None = None, + session_options: dict[str, t.Any] | None = None, + query_class: type[Query] = Query, + model_class: _FSA_MCT = Model, # type: ignore[assignment] + engine_options: dict[str, t.Any] | None = None, + add_models_to_shell: bool = True, + disable_autonaming: bool = False, + ): + if session_options is None: + session_options = {} + + self.Query = query_class + """The default query class used by ``Model.query`` and ``lazy="dynamic"`` + relationships. + + .. warning:: + The query interface is considered legacy in SQLAlchemy. + + Customize this by passing the ``query_class`` parameter to the extension. + """ + + self.session = self._make_scoped_session(session_options) + """A :class:`sqlalchemy.orm.scoping.scoped_session` that creates instances of + :class:`.Session` scoped to the current Flask application context. The session + will be removed, returning the engine connection to the pool, when the + application context exits. + + Customize this by passing ``session_options`` to the extension. + + This requires that a Flask application context is active. + + .. versionchanged:: 3.0 + The session is scoped to the current app context. + """ + + self.metadatas: dict[str | None, sa.MetaData] = {} + """Map of bind keys to :class:`sqlalchemy.schema.MetaData` instances. The + ``None`` key refers to the default metadata, and is available as + :attr:`metadata`. + + Customize the default metadata by passing the ``metadata`` parameter to the + extension. This can be used to set a naming convention. When metadata for + another bind key is created, it copies the default's naming convention. + + .. versionadded:: 3.0 + """ + + if metadata is not None: + if len(_get_2x_declarative_bases(model_class)) > 0: + warnings.warn( + "When using SQLAlchemy 2.x style of declarative classes," + " the `metadata` should be an attribute of the base class." + "The metadata passed into SQLAlchemy() is ignored.", + DeprecationWarning, + stacklevel=2, + ) + else: + metadata.info["bind_key"] = None + self.metadatas[None] = metadata + + self.Table = self._make_table_class() + """A :class:`sqlalchemy.schema.Table` class that chooses a metadata + automatically. + + Unlike the base ``Table``, the ``metadata`` argument is not required. If it is + not given, it is selected based on the ``bind_key`` argument. + + :param bind_key: Used to select a different metadata. + :param args: Arguments passed to the base class. These are typically the table's + name, columns, and constraints. + :param kwargs: Arguments passed to the base class. + + .. versionchanged:: 3.0 + This is a subclass of SQLAlchemy's ``Table`` rather than a function. + """ + + self.Model = self._make_declarative_base( + model_class, disable_autonaming=disable_autonaming + ) + """A SQLAlchemy declarative model class. Subclass this to define database + models. + + If a model does not set ``__tablename__``, it will be generated by converting + the class name from ``CamelCase`` to ``snake_case``. It will not be generated + if the model looks like it uses single-table inheritance. + + If a model or parent class sets ``__bind_key__``, it will use that metadata and + database engine. Otherwise, it will use the default :attr:`metadata` and + :attr:`engine`. This is ignored if the model sets ``metadata`` or ``__table__``. + + For code using the SQLAlchemy 1.x API, customize this model by subclassing + :class:`.Model` and passing the ``model_class`` parameter to the extension. + A fully created declarative model class can be + passed as well, to use a custom metaclass. + + For code using the SQLAlchemy 2.x API, customize this model by subclassing + :class:`sqlalchemy.orm.DeclarativeBase` or + :class:`sqlalchemy.orm.DeclarativeBaseNoMeta` + and passing the ``model_class`` parameter to the extension. + """ + + if engine_options is None: + engine_options = {} + + self._engine_options = engine_options + self._app_engines: WeakKeyDictionary[Flask, dict[str | None, sa.engine.Engine]] + self._app_engines = WeakKeyDictionary() + self._add_models_to_shell = add_models_to_shell + + if app is not None: + self.init_app(app) + + def __repr__(self) -> str: + if not has_app_context(): + return f"<{type(self).__name__}>" + + message = f"{type(self).__name__} {self.engine.url}" + + if len(self.engines) > 1: + message = f"{message} +{len(self.engines) - 1}" + + return f"<{message}>" + + def init_app(self, app: Flask) -> None: + """Initialize a Flask application for use with this extension instance. This + must be called before accessing the database engine or session with the app. + + This sets default configuration values, then configures the extension on the + application and creates the engines for each bind key. Therefore, this must be + called after the application has been configured. Changes to application config + after this call will not be reflected. + + The following keys from ``app.config`` are used: + + - :data:`.SQLALCHEMY_DATABASE_URI` + - :data:`.SQLALCHEMY_ENGINE_OPTIONS` + - :data:`.SQLALCHEMY_ECHO` + - :data:`.SQLALCHEMY_BINDS` + - :data:`.SQLALCHEMY_RECORD_QUERIES` + - :data:`.SQLALCHEMY_TRACK_MODIFICATIONS` + + :param app: The Flask application to initialize. + """ + if "sqlalchemy" in app.extensions: + raise RuntimeError( + "A 'SQLAlchemy' instance has already been registered on this Flask app." + " Import and use that instance instead." + ) + + app.extensions["sqlalchemy"] = self + app.teardown_appcontext(self._teardown_session) + + if self._add_models_to_shell: + from .cli import add_models_to_shell + + app.shell_context_processor(add_models_to_shell) + + basic_uri: str | sa.engine.URL | None = app.config.setdefault( + "SQLALCHEMY_DATABASE_URI", None + ) + basic_engine_options = self._engine_options.copy() + basic_engine_options.update( + app.config.setdefault("SQLALCHEMY_ENGINE_OPTIONS", {}) + ) + echo: bool = app.config.setdefault("SQLALCHEMY_ECHO", False) + config_binds: dict[ + str | None, str | sa.engine.URL | dict[str, t.Any] + ] = app.config.setdefault("SQLALCHEMY_BINDS", {}) + engine_options: dict[str | None, dict[str, t.Any]] = {} + + # Build the engine config for each bind key. + for key, value in config_binds.items(): + engine_options[key] = self._engine_options.copy() + + if isinstance(value, (str, sa.engine.URL)): + engine_options[key]["url"] = value + else: + engine_options[key].update(value) + + # Build the engine config for the default bind key. + if basic_uri is not None: + basic_engine_options["url"] = basic_uri + + if "url" in basic_engine_options: + engine_options.setdefault(None, {}).update(basic_engine_options) + + if not engine_options: + raise RuntimeError( + "Either 'SQLALCHEMY_DATABASE_URI' or 'SQLALCHEMY_BINDS' must be set." + ) + + engines = self._app_engines.setdefault(app, {}) + + # Dispose existing engines in case init_app is called again. + if engines: + for engine in engines.values(): + engine.dispose() + + engines.clear() + + # Create the metadata and engine for each bind key. + for key, options in engine_options.items(): + self._make_metadata(key) + options.setdefault("echo", echo) + options.setdefault("echo_pool", echo) + self._apply_driver_defaults(options, app) + engines[key] = self._make_engine(key, options, app) + + if app.config.setdefault("SQLALCHEMY_RECORD_QUERIES", False): + from . import record_queries + + for engine in engines.values(): + record_queries._listen(engine) + + if app.config.setdefault("SQLALCHEMY_TRACK_MODIFICATIONS", False): + from . import track_modifications + + track_modifications._listen(self.session) + + def _make_scoped_session( + self, options: dict[str, t.Any] + ) -> sa_orm.scoped_session[Session]: + """Create a :class:`sqlalchemy.orm.scoping.scoped_session` around the factory + from :meth:`_make_session_factory`. The result is available as :attr:`session`. + + The scope function can be customized using the ``scopefunc`` key in the + ``session_options`` parameter to the extension. By default it uses the current + thread or greenlet id. + + This method is used for internal setup. Its signature may change at any time. + + :meta private: + + :param options: The ``session_options`` parameter from ``__init__``. Keyword + arguments passed to the session factory. A ``scopefunc`` key is popped. + + .. versionchanged:: 3.0 + The session is scoped to the current app context. + + .. versionchanged:: 3.0 + Renamed from ``create_scoped_session``, this method is internal. + """ + scope = options.pop("scopefunc", _app_ctx_id) + factory = self._make_session_factory(options) + return sa_orm.scoped_session(factory, scope) + + def _make_session_factory( + self, options: dict[str, t.Any] + ) -> sa_orm.sessionmaker[Session]: + """Create the SQLAlchemy :class:`sqlalchemy.orm.sessionmaker` used by + :meth:`_make_scoped_session`. + + To customize, pass the ``session_options`` parameter to :class:`SQLAlchemy`. To + customize the session class, subclass :class:`.Session` and pass it as the + ``class_`` key. + + This method is used for internal setup. Its signature may change at any time. + + :meta private: + + :param options: The ``session_options`` parameter from ``__init__``. Keyword + arguments passed to the session factory. + + .. versionchanged:: 3.0 + The session class can be customized. + + .. versionchanged:: 3.0 + Renamed from ``create_session``, this method is internal. + """ + options.setdefault("class_", Session) + options.setdefault("query_cls", self.Query) + return sa_orm.sessionmaker(db=self, **options) + + def _teardown_session(self, exc: BaseException | None) -> None: + """Remove the current session at the end of the request. + + :meta private: + + .. versionadded:: 3.0 + """ + self.session.remove() + + def _make_metadata(self, bind_key: str | None) -> sa.MetaData: + """Get or create a :class:`sqlalchemy.schema.MetaData` for the given bind key. + + This method is used for internal setup. Its signature may change at any time. + + :meta private: + + :param bind_key: The name of the metadata being created. + + .. versionadded:: 3.0 + """ + if bind_key in self.metadatas: + return self.metadatas[bind_key] + + if bind_key is not None: + # Copy the naming convention from the default metadata. + naming_convention = self._make_metadata(None).naming_convention + else: + naming_convention = None + + # Set the bind key in info to be used by session.get_bind. + metadata = sa.MetaData( + naming_convention=naming_convention, info={"bind_key": bind_key} + ) + self.metadatas[bind_key] = metadata + return metadata + + def _make_table_class(self) -> type[_Table]: + """Create a SQLAlchemy :class:`sqlalchemy.schema.Table` class that chooses a + metadata automatically based on the ``bind_key``. The result is available as + :attr:`Table`. + + This method is used for internal setup. Its signature may change at any time. + + :meta private: + + .. versionadded:: 3.0 + """ + + class Table(_Table): + def __new__( + cls, *args: t.Any, bind_key: str | None = None, **kwargs: t.Any + ) -> Table: + # If a metadata arg is passed, go directly to the base Table. Also do + # this for no args so the correct error is shown. + if not args or (len(args) >= 2 and isinstance(args[1], sa.MetaData)): + return super().__new__(cls, *args, **kwargs) + + metadata = self._make_metadata(bind_key) + return super().__new__(cls, *[args[0], metadata, *args[1:]], **kwargs) + + return Table + + def _make_declarative_base( + self, + model_class: _FSA_MCT, + disable_autonaming: bool = False, + ) -> t.Type[_FSAModel]: + """Create a SQLAlchemy declarative model class. The result is available as + :attr:`Model`. + + To customize, subclass :class:`.Model` and pass it as ``model_class`` to + :class:`SQLAlchemy`. To customize at the metaclass level, pass an already + created declarative model class as ``model_class``. + + This method is used for internal setup. Its signature may change at any time. + + :meta private: + + :param model_class: A model base class, or an already created declarative model + class. + + :param disable_autonaming: Turns off automatic tablename generation in models. + + .. versionchanged:: 3.1.0 + Added support for passing SQLAlchemy 2.x base class as model class. + Added optional ``disable_autonaming`` parameter. + + .. versionchanged:: 3.0 + Renamed with a leading underscore, this method is internal. + + .. versionchanged:: 2.3 + ``model`` can be an already created declarative model class. + """ + model: t.Type[_FSAModel] + declarative_bases = _get_2x_declarative_bases(model_class) + if len(declarative_bases) > 1: + # raise error if more than one declarative base is found + raise ValueError( + "Only one declarative base can be passed to SQLAlchemy." + " Got: {}".format(model_class.__bases__) + ) + elif len(declarative_bases) == 1: + body = dict(model_class.__dict__) + body["__fsa__"] = self + mixin_classes = [BindMixin, NameMixin, Model] + if disable_autonaming: + mixin_classes.remove(NameMixin) + model = types.new_class( + "FlaskSQLAlchemyBase", + (*mixin_classes, *model_class.__bases__), + {"metaclass": type(declarative_bases[0])}, + lambda ns: ns.update(body), + ) + elif not isinstance(model_class, sa_orm.DeclarativeMeta): + metadata = self._make_metadata(None) + metaclass = DefaultMetaNoName if disable_autonaming else DefaultMeta + model = sa_orm.declarative_base( + metadata=metadata, cls=model_class, name="Model", metaclass=metaclass + ) + else: + model = model_class # type: ignore[assignment] + + if None not in self.metadatas: + # Use the model's metadata as the default metadata. + model.metadata.info["bind_key"] = None + self.metadatas[None] = model.metadata + else: + # Use the passed in default metadata as the model's metadata. + model.metadata = self.metadatas[None] + + model.query_class = self.Query + model.query = _QueryProperty() # type: ignore[assignment] + model.__fsa__ = self + return model + + def _apply_driver_defaults(self, options: dict[str, t.Any], app: Flask) -> None: + """Apply driver-specific configuration to an engine. + + SQLite in-memory databases use ``StaticPool`` and disable ``check_same_thread``. + File paths are relative to the app's :attr:`~flask.Flask.instance_path`, + which is created if it doesn't exist. + + MySQL sets ``charset="utf8mb4"``, and ``pool_timeout`` defaults to 2 hours. + + This method is used for internal setup. Its signature may change at any time. + + :meta private: + + :param options: Arguments passed to the engine. + :param app: The application that the engine configuration belongs to. + + .. versionchanged:: 3.0 + SQLite paths are relative to ``app.instance_path``. It does not use + ``NullPool`` if ``pool_size`` is 0. Driver-level URIs are supported. + + .. versionchanged:: 3.0 + MySQL sets ``charset="utf8mb4". It does not set ``pool_size`` to 10. It + does not set ``pool_recycle`` if not using a queue pool. + + .. versionchanged:: 3.0 + Renamed from ``apply_driver_hacks``, this method is internal. It does not + return anything. + + .. versionchanged:: 2.5 + Returns ``(sa_url, options)``. + """ + url = sa.engine.make_url(options["url"]) + + if url.drivername in {"sqlite", "sqlite+pysqlite"}: + if url.database is None or url.database in {"", ":memory:"}: + options["poolclass"] = sa.pool.StaticPool + + if "connect_args" not in options: + options["connect_args"] = {} + + options["connect_args"]["check_same_thread"] = False + else: + # the url might look like sqlite:///file:path?uri=true + is_uri = url.query.get("uri", False) + + if is_uri: + db_str = url.database[5:] + else: + db_str = url.database + + if not os.path.isabs(db_str): + os.makedirs(app.instance_path, exist_ok=True) + db_str = os.path.join(app.instance_path, db_str) + + if is_uri: + db_str = f"file:{db_str}" + + options["url"] = url.set(database=db_str) + elif url.drivername.startswith("mysql"): + # set queue defaults only when using queue pool + if ( + "pool_class" not in options + or options["pool_class"] is sa.pool.QueuePool + ): + options.setdefault("pool_recycle", 7200) + + if "charset" not in url.query: + options["url"] = url.update_query_dict({"charset": "utf8mb4"}) + + def _make_engine( + self, bind_key: str | None, options: dict[str, t.Any], app: Flask + ) -> sa.engine.Engine: + """Create the :class:`sqlalchemy.engine.Engine` for the given bind key and app. + + To customize, use :data:`.SQLALCHEMY_ENGINE_OPTIONS` or + :data:`.SQLALCHEMY_BINDS` config. Pass ``engine_options`` to :class:`SQLAlchemy` + to set defaults for all engines. + + This method is used for internal setup. Its signature may change at any time. + + :meta private: + + :param bind_key: The name of the engine being created. + :param options: Arguments passed to the engine. + :param app: The application that the engine configuration belongs to. + + .. versionchanged:: 3.0 + Renamed from ``create_engine``, this method is internal. + """ + return sa.engine_from_config(options, prefix="") + + @property + def metadata(self) -> sa.MetaData: + """The default metadata used by :attr:`Model` and :attr:`Table` if no bind key + is set. + """ + return self.metadatas[None] + + @property + def engines(self) -> t.Mapping[str | None, sa.engine.Engine]: + """Map of bind keys to :class:`sqlalchemy.engine.Engine` instances for current + application. The ``None`` key refers to the default engine, and is available as + :attr:`engine`. + + To customize, set the :data:`.SQLALCHEMY_BINDS` config, and set defaults by + passing the ``engine_options`` parameter to the extension. + + This requires that a Flask application context is active. + + .. versionadded:: 3.0 + """ + app = current_app._get_current_object() # type: ignore[attr-defined] + + if app not in self._app_engines: + raise RuntimeError( + "The current Flask app is not registered with this 'SQLAlchemy'" + " instance. Did you forget to call 'init_app', or did you create" + " multiple 'SQLAlchemy' instances?" + ) + + return self._app_engines[app] + + @property + def engine(self) -> sa.engine.Engine: + """The default :class:`~sqlalchemy.engine.Engine` for the current application, + used by :attr:`session` if the :attr:`Model` or :attr:`Table` being queried does + not set a bind key. + + To customize, set the :data:`.SQLALCHEMY_ENGINE_OPTIONS` config, and set + defaults by passing the ``engine_options`` parameter to the extension. + + This requires that a Flask application context is active. + """ + return self.engines[None] + + def get_engine( + self, bind_key: str | None = None, **kwargs: t.Any + ) -> sa.engine.Engine: + """Get the engine for the given bind key for the current application. + This requires that a Flask application context is active. + + :param bind_key: The name of the engine. + + .. deprecated:: 3.0 + Will be removed in Flask-SQLAlchemy 3.2. Use ``engines[key]`` instead. + + .. versionchanged:: 3.0 + Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app`` + parameter. + """ + warnings.warn( + "'get_engine' is deprecated and will be removed in Flask-SQLAlchemy" + " 3.2. Use 'engine' or 'engines[key]' instead. If you're using" + " Flask-Migrate or Alembic, you'll need to update your 'env.py' file.", + DeprecationWarning, + stacklevel=2, + ) + + if "bind" in kwargs: + bind_key = kwargs.pop("bind") + + return self.engines[bind_key] + + def get_or_404( + self, + entity: type[_O], + ident: t.Any, + *, + description: str | None = None, + **kwargs: t.Any, + ) -> _O: + """Like :meth:`session.get() ` but aborts with a + ``404 Not Found`` error instead of returning ``None``. + + :param entity: The model class to query. + :param ident: The primary key to query. + :param description: A custom message to show on the error page. + :param kwargs: Extra arguments passed to ``session.get()``. + + .. versionchanged:: 3.1 + Pass extra keyword arguments to ``session.get()``. + + .. versionadded:: 3.0 + """ + value = self.session.get(entity, ident, **kwargs) + + if value is None: + abort(404, description=description) + + return value + + def first_or_404( + self, statement: sa.sql.Select[t.Any], *, description: str | None = None + ) -> t.Any: + """Like :meth:`Result.scalar() `, but aborts + with a ``404 Not Found`` error instead of returning ``None``. + + :param statement: The ``select`` statement to execute. + :param description: A custom message to show on the error page. + + .. versionadded:: 3.0 + """ + value = self.session.execute(statement).scalar() + + if value is None: + abort(404, description=description) + + return value + + def one_or_404( + self, statement: sa.sql.Select[t.Any], *, description: str | None = None + ) -> t.Any: + """Like :meth:`Result.scalar_one() `, + but aborts with a ``404 Not Found`` error instead of raising ``NoResultFound`` + or ``MultipleResultsFound``. + + :param statement: The ``select`` statement to execute. + :param description: A custom message to show on the error page. + + .. versionadded:: 3.0 + """ + try: + return self.session.execute(statement).scalar_one() + except (sa_exc.NoResultFound, sa_exc.MultipleResultsFound): + abort(404, description=description) + + def paginate( + self, + select: sa.sql.Select[t.Any], + *, + page: int | None = None, + per_page: int | None = None, + max_per_page: int | None = None, + error_out: bool = True, + count: bool = True, + ) -> Pagination: + """Apply an offset and limit to a select statment based on the current page and + number of items per page, returning a :class:`.Pagination` object. + + The statement should select a model class, like ``select(User)``. This applies + ``unique()`` and ``scalars()`` modifiers to the result, so compound selects will + not return the expected results. + + :param select: The ``select`` statement to paginate. + :param page: The current page, used to calculate the offset. Defaults to the + ``page`` query arg during a request, or 1 otherwise. + :param per_page: The maximum number of items on a page, used to calculate the + offset and limit. Defaults to the ``per_page`` query arg during a request, + or 20 otherwise. + :param max_per_page: The maximum allowed value for ``per_page``, to limit a + user-provided value. Use ``None`` for no limit. Defaults to 100. + :param error_out: Abort with a ``404 Not Found`` error if no items are returned + and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if + either are not ints. + :param count: Calculate the total number of values by issuing an extra count + query. For very complex queries this may be inaccurate or slow, so it can be + disabled and set manually if necessary. + + .. versionchanged:: 3.0 + The ``count`` query is more efficient. + + .. versionadded:: 3.0 + """ + return SelectPagination( + select=select, + session=self.session(), + page=page, + per_page=per_page, + max_per_page=max_per_page, + error_out=error_out, + count=count, + ) + + def _call_for_binds( + self, bind_key: str | None | list[str | None], op_name: str + ) -> None: + """Call a method on each metadata. + + :meta private: + + :param bind_key: A bind key or list of keys. Defaults to all binds. + :param op_name: The name of the method to call. + + .. versionchanged:: 3.0 + Renamed from ``_execute_for_all_tables``. + """ + if bind_key == "__all__": + keys: list[str | None] = list(self.metadatas) + elif bind_key is None or isinstance(bind_key, str): + keys = [bind_key] + else: + keys = bind_key + + for key in keys: + try: + engine = self.engines[key] + except KeyError: + message = f"Bind key '{key}' is not in 'SQLALCHEMY_BINDS' config." + + if key is None: + message = f"'SQLALCHEMY_DATABASE_URI' config is not set. {message}" + + raise sa_exc.UnboundExecutionError(message) from None + + metadata = self.metadatas[key] + getattr(metadata, op_name)(bind=engine) + + def create_all(self, bind_key: str | None | list[str | None] = "__all__") -> None: + """Create tables that do not exist in the database by calling + ``metadata.create_all()`` for all or some bind keys. This does not + update existing tables, use a migration library for that. + + This requires that a Flask application context is active. + + :param bind_key: A bind key or list of keys to create the tables for. Defaults + to all binds. + + .. versionchanged:: 3.0 + Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app`` + parameter. + + .. versionchanged:: 0.12 + Added the ``bind`` and ``app`` parameters. + """ + self._call_for_binds(bind_key, "create_all") + + def drop_all(self, bind_key: str | None | list[str | None] = "__all__") -> None: + """Drop tables by calling ``metadata.drop_all()`` for all or some bind keys. + + This requires that a Flask application context is active. + + :param bind_key: A bind key or list of keys to drop the tables from. Defaults to + all binds. + + .. versionchanged:: 3.0 + Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app`` + parameter. + + .. versionchanged:: 0.12 + Added the ``bind`` and ``app`` parameters. + """ + self._call_for_binds(bind_key, "drop_all") + + def reflect(self, bind_key: str | None | list[str | None] = "__all__") -> None: + """Load table definitions from the database by calling ``metadata.reflect()`` + for all or some bind keys. + + This requires that a Flask application context is active. + + :param bind_key: A bind key or list of keys to reflect the tables from. Defaults + to all binds. + + .. versionchanged:: 3.0 + Renamed the ``bind`` parameter to ``bind_key``. Removed the ``app`` + parameter. + + .. versionchanged:: 0.12 + Added the ``bind`` and ``app`` parameters. + """ + self._call_for_binds(bind_key, "reflect") + + def _set_rel_query(self, kwargs: dict[str, t.Any]) -> None: + """Apply the extension's :attr:`Query` class as the default for relationships + and backrefs. + + :meta private: + """ + kwargs.setdefault("query_class", self.Query) + + if "backref" in kwargs: + backref = kwargs["backref"] + + if isinstance(backref, str): + backref = (backref, {}) + + backref[1].setdefault("query_class", self.Query) + + def relationship( + self, *args: t.Any, **kwargs: t.Any + ) -> sa_orm.RelationshipProperty[t.Any]: + """A :func:`sqlalchemy.orm.relationship` that applies this extension's + :attr:`Query` class for dynamic relationships and backrefs. + + .. versionchanged:: 3.0 + The :attr:`Query` class is set on ``backref``. + """ + self._set_rel_query(kwargs) + return sa_orm.relationship(*args, **kwargs) + + def dynamic_loader( + self, argument: t.Any, **kwargs: t.Any + ) -> sa_orm.RelationshipProperty[t.Any]: + """A :func:`sqlalchemy.orm.dynamic_loader` that applies this extension's + :attr:`Query` class for relationships and backrefs. + + .. versionchanged:: 3.0 + The :attr:`Query` class is set on ``backref``. + """ + self._set_rel_query(kwargs) + return sa_orm.dynamic_loader(argument, **kwargs) + + def _relation( + self, *args: t.Any, **kwargs: t.Any + ) -> sa_orm.RelationshipProperty[t.Any]: + """A :func:`sqlalchemy.orm.relationship` that applies this extension's + :attr:`Query` class for dynamic relationships and backrefs. + + SQLAlchemy 2.0 removes this name, use ``relationship`` instead. + + :meta private: + + .. versionchanged:: 3.0 + The :attr:`Query` class is set on ``backref``. + """ + self._set_rel_query(kwargs) + f = sa_orm.relationship + return f(*args, **kwargs) + + def __getattr__(self, name: str) -> t.Any: + if name == "relation": + return self._relation + + if name == "event": + return sa_event + + if name.startswith("_"): + raise AttributeError(name) + + for mod in (sa, sa_orm): + if hasattr(mod, name): + return getattr(mod, name) + + raise AttributeError(name) diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/model.py b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/model.py new file mode 100644 index 0000000..c6f9e5a --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/model.py @@ -0,0 +1,330 @@ +from __future__ import annotations + +import re +import typing as t + +import sqlalchemy as sa +import sqlalchemy.orm as sa_orm + +from .query import Query + +if t.TYPE_CHECKING: + from .extension import SQLAlchemy + + +class _QueryProperty: + """A class property that creates a query object for a model. + + :meta private: + """ + + def __get__(self, obj: Model | None, cls: type[Model]) -> Query: + return cls.query_class( + cls, session=cls.__fsa__.session() # type: ignore[arg-type] + ) + + +class Model: + """The base class of the :attr:`.SQLAlchemy.Model` declarative model class. + + To define models, subclass :attr:`db.Model <.SQLAlchemy.Model>`, not this. To + customize ``db.Model``, subclass this and pass it as ``model_class`` to + :class:`.SQLAlchemy`. To customize ``db.Model`` at the metaclass level, pass an + already created declarative model class as ``model_class``. + """ + + __fsa__: t.ClassVar[SQLAlchemy] + """Internal reference to the extension object. + + :meta private: + """ + + query_class: t.ClassVar[type[Query]] = Query + """Query class used by :attr:`query`. Defaults to :attr:`.SQLAlchemy.Query`, which + defaults to :class:`.Query`. + """ + + query: t.ClassVar[Query] = _QueryProperty() # type: ignore[assignment] + """A SQLAlchemy query for a model. Equivalent to ``db.session.query(Model)``. Can be + customized per-model by overriding :attr:`query_class`. + + .. warning:: + The query interface is considered legacy in SQLAlchemy. Prefer using + ``session.execute(select())`` instead. + """ + + def __repr__(self) -> str: + state = sa.inspect(self) + assert state is not None + + if state.transient: + pk = f"(transient {id(self)})" + elif state.pending: + pk = f"(pending {id(self)})" + else: + pk = ", ".join(map(str, state.identity)) + + return f"<{type(self).__name__} {pk}>" + + +class BindMetaMixin(type): + """Metaclass mixin that sets a model's ``metadata`` based on its ``__bind_key__``. + + If the model sets ``metadata`` or ``__table__`` directly, ``__bind_key__`` is + ignored. If the ``metadata`` is the same as the parent model, it will not be set + directly on the child model. + """ + + __fsa__: SQLAlchemy + metadata: sa.MetaData + + def __init__( + cls, name: str, bases: tuple[type, ...], d: dict[str, t.Any], **kwargs: t.Any + ) -> None: + if not ("metadata" in cls.__dict__ or "__table__" in cls.__dict__): + bind_key = getattr(cls, "__bind_key__", None) + parent_metadata = getattr(cls, "metadata", None) + metadata = cls.__fsa__._make_metadata(bind_key) + + if metadata is not parent_metadata: + cls.metadata = metadata + + super().__init__(name, bases, d, **kwargs) + + +class BindMixin: + """DeclarativeBase mixin to set a model's ``metadata`` based on ``__bind_key__``. + + If no ``__bind_key__`` is specified, the model will use the default metadata + provided by ``DeclarativeBase`` or ``DeclarativeBaseNoMeta``. + If the model doesn't set ``metadata`` or ``__table__`` directly + and does set ``__bind_key__``, the model will use the metadata + for the specified bind key. + If the ``metadata`` is the same as the parent model, it will not be set + directly on the child model. + + .. versionchanged:: 3.1.0 + """ + + __fsa__: SQLAlchemy + metadata: sa.MetaData + + @classmethod + def __init_subclass__(cls: t.Type[BindMixin], **kwargs: t.Dict[str, t.Any]) -> None: + if not ("metadata" in cls.__dict__ or "__table__" in cls.__dict__) and hasattr( + cls, "__bind_key__" + ): + bind_key = getattr(cls, "__bind_key__", None) + parent_metadata = getattr(cls, "metadata", None) + metadata = cls.__fsa__._make_metadata(bind_key) + + if metadata is not parent_metadata: + cls.metadata = metadata + + super().__init_subclass__(**kwargs) + + +class NameMetaMixin(type): + """Metaclass mixin that sets a model's ``__tablename__`` by converting the + ``CamelCase`` class name to ``snake_case``. A name is set for non-abstract models + that do not otherwise define ``__tablename__``. If a model does not define a primary + key, it will not generate a name or ``__table__``, for single-table inheritance. + """ + + metadata: sa.MetaData + __tablename__: str + __table__: sa.Table + + def __init__( + cls, name: str, bases: tuple[type, ...], d: dict[str, t.Any], **kwargs: t.Any + ) -> None: + if should_set_tablename(cls): + cls.__tablename__ = camel_to_snake_case(cls.__name__) + + super().__init__(name, bases, d, **kwargs) + + # __table_cls__ has run. If no table was created, use the parent table. + if ( + "__tablename__" not in cls.__dict__ + and "__table__" in cls.__dict__ + and cls.__dict__["__table__"] is None + ): + del cls.__table__ + + def __table_cls__(cls, *args: t.Any, **kwargs: t.Any) -> sa.Table | None: + """This is called by SQLAlchemy during mapper setup. It determines the final + table object that the model will use. + + If no primary key is found, that indicates single-table inheritance, so no table + will be created and ``__tablename__`` will be unset. + """ + schema = kwargs.get("schema") + + if schema is None: + key = args[0] + else: + key = f"{schema}.{args[0]}" + + # Check if a table with this name already exists. Allows reflected tables to be + # applied to models by name. + if key in cls.metadata.tables: + return sa.Table(*args, **kwargs) + + # If a primary key is found, create a table for joined-table inheritance. + for arg in args: + if (isinstance(arg, sa.Column) and arg.primary_key) or isinstance( + arg, sa.PrimaryKeyConstraint + ): + return sa.Table(*args, **kwargs) + + # If no base classes define a table, return one that's missing a primary key + # so SQLAlchemy shows the correct error. + for base in cls.__mro__[1:-1]: + if "__table__" in base.__dict__: + break + else: + return sa.Table(*args, **kwargs) + + # Single-table inheritance, use the parent table name. __init__ will unset + # __table__ based on this. + if "__tablename__" in cls.__dict__: + del cls.__tablename__ + + return None + + +class NameMixin: + """DeclarativeBase mixin that sets a model's ``__tablename__`` by converting the + ``CamelCase`` class name to ``snake_case``. A name is set for non-abstract models + that do not otherwise define ``__tablename__``. If a model does not define a primary + key, it will not generate a name or ``__table__``, for single-table inheritance. + + .. versionchanged:: 3.1.0 + """ + + metadata: sa.MetaData + __tablename__: str + __table__: sa.Table + + @classmethod + def __init_subclass__(cls: t.Type[NameMixin], **kwargs: t.Dict[str, t.Any]) -> None: + if should_set_tablename(cls): + cls.__tablename__ = camel_to_snake_case(cls.__name__) + + super().__init_subclass__(**kwargs) + + # __table_cls__ has run. If no table was created, use the parent table. + if ( + "__tablename__" not in cls.__dict__ + and "__table__" in cls.__dict__ + and cls.__dict__["__table__"] is None + ): + del cls.__table__ + + @classmethod + def __table_cls__(cls, *args: t.Any, **kwargs: t.Any) -> sa.Table | None: + """This is called by SQLAlchemy during mapper setup. It determines the final + table object that the model will use. + + If no primary key is found, that indicates single-table inheritance, so no table + will be created and ``__tablename__`` will be unset. + """ + schema = kwargs.get("schema") + + if schema is None: + key = args[0] + else: + key = f"{schema}.{args[0]}" + + # Check if a table with this name already exists. Allows reflected tables to be + # applied to models by name. + if key in cls.metadata.tables: + return sa.Table(*args, **kwargs) + + # If a primary key is found, create a table for joined-table inheritance. + for arg in args: + if (isinstance(arg, sa.Column) and arg.primary_key) or isinstance( + arg, sa.PrimaryKeyConstraint + ): + return sa.Table(*args, **kwargs) + + # If no base classes define a table, return one that's missing a primary key + # so SQLAlchemy shows the correct error. + for base in cls.__mro__[1:-1]: + if "__table__" in base.__dict__: + break + else: + return sa.Table(*args, **kwargs) + + # Single-table inheritance, use the parent table name. __init__ will unset + # __table__ based on this. + if "__tablename__" in cls.__dict__: + del cls.__tablename__ + + return None + + +def should_set_tablename(cls: type) -> bool: + """Determine whether ``__tablename__`` should be generated for a model. + + - If no class in the MRO sets a name, one should be generated. + - If a declared attr is found, it should be used instead. + - If a name is found, it should be used if the class is a mixin, otherwise one + should be generated. + - Abstract models should not have one generated. + + Later, ``__table_cls__`` will determine if the model looks like single or + joined-table inheritance. If no primary key is found, the name will be unset. + """ + if ( + cls.__dict__.get("__abstract__", False) + or ( + not issubclass(cls, (sa_orm.DeclarativeBase, sa_orm.DeclarativeBaseNoMeta)) + and not any(isinstance(b, sa_orm.DeclarativeMeta) for b in cls.__mro__[1:]) + ) + or any( + (b is sa_orm.DeclarativeBase or b is sa_orm.DeclarativeBaseNoMeta) + for b in cls.__bases__ + ) + ): + return False + + for base in cls.__mro__: + if "__tablename__" not in base.__dict__: + continue + + if isinstance(base.__dict__["__tablename__"], sa_orm.declared_attr): + return False + + return not ( + base is cls + or base.__dict__.get("__abstract__", False) + or not ( + # SQLAlchemy 1.x + isinstance(base, sa_orm.DeclarativeMeta) + # 2.x: DeclarativeBas uses this as metaclass + or isinstance(base, sa_orm.decl_api.DeclarativeAttributeIntercept) + # 2.x: DeclarativeBaseNoMeta doesn't use a metaclass + or issubclass(base, sa_orm.DeclarativeBaseNoMeta) + ) + ) + + return True + + +def camel_to_snake_case(name: str) -> str: + """Convert a ``CamelCase`` name to ``snake_case``.""" + name = re.sub(r"((?<=[a-z0-9])[A-Z]|(?!^)[A-Z](?=[a-z]))", r"_\1", name) + return name.lower().lstrip("_") + + +class DefaultMeta(BindMetaMixin, NameMetaMixin, sa_orm.DeclarativeMeta): + """SQLAlchemy declarative metaclass that provides ``__bind_key__`` and + ``__tablename__`` support. + """ + + +class DefaultMetaNoName(BindMetaMixin, sa_orm.DeclarativeMeta): + """SQLAlchemy declarative metaclass that provides ``__bind_key__`` and + ``__tablename__`` support. + """ diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py new file mode 100644 index 0000000..3d49d6e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/pagination.py @@ -0,0 +1,364 @@ +from __future__ import annotations + +import typing as t +from math import ceil + +import sqlalchemy as sa +import sqlalchemy.orm as sa_orm +from flask import abort +from flask import request + + +class Pagination: + """Apply an offset and limit to the query based on the current page and number of + items per page. + + Don't create pagination objects manually. They are created by + :meth:`.SQLAlchemy.paginate` and :meth:`.Query.paginate`. + + This is a base class, a subclass must implement :meth:`_query_items` and + :meth:`_query_count`. Those methods will use arguments passed as ``kwargs`` to + perform the queries. + + :param page: The current page, used to calculate the offset. Defaults to the + ``page`` query arg during a request, or 1 otherwise. + :param per_page: The maximum number of items on a page, used to calculate the + offset and limit. Defaults to the ``per_page`` query arg during a request, + or 20 otherwise. + :param max_per_page: The maximum allowed value for ``per_page``, to limit a + user-provided value. Use ``None`` for no limit. Defaults to 100. + :param error_out: Abort with a ``404 Not Found`` error if no items are returned + and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if + either are not ints. + :param count: Calculate the total number of values by issuing an extra count + query. For very complex queries this may be inaccurate or slow, so it can be + disabled and set manually if necessary. + :param kwargs: Information about the query to paginate. Different subclasses will + require different arguments. + + .. versionchanged:: 3.0 + Iterating over a pagination object iterates over its items. + + .. versionchanged:: 3.0 + Creating instances manually is not a public API. + """ + + def __init__( + self, + page: int | None = None, + per_page: int | None = None, + max_per_page: int | None = 100, + error_out: bool = True, + count: bool = True, + **kwargs: t.Any, + ) -> None: + self._query_args = kwargs + page, per_page = self._prepare_page_args( + page=page, + per_page=per_page, + max_per_page=max_per_page, + error_out=error_out, + ) + + self.page: int = page + """The current page.""" + + self.per_page: int = per_page + """The maximum number of items on a page.""" + + self.max_per_page: int | None = max_per_page + """The maximum allowed value for ``per_page``.""" + + items = self._query_items() + + if not items and page != 1 and error_out: + abort(404) + + self.items: list[t.Any] = items + """The items on the current page. Iterating over the pagination object is + equivalent to iterating over the items. + """ + + if count: + total = self._query_count() + else: + total = None + + self.total: int | None = total + """The total number of items across all pages.""" + + @staticmethod + def _prepare_page_args( + *, + page: int | None = None, + per_page: int | None = None, + max_per_page: int | None = None, + error_out: bool = True, + ) -> tuple[int, int]: + if request: + if page is None: + try: + page = int(request.args.get("page", 1)) + except (TypeError, ValueError): + if error_out: + abort(404) + + page = 1 + + if per_page is None: + try: + per_page = int(request.args.get("per_page", 20)) + except (TypeError, ValueError): + if error_out: + abort(404) + + per_page = 20 + else: + if page is None: + page = 1 + + if per_page is None: + per_page = 20 + + if max_per_page is not None: + per_page = min(per_page, max_per_page) + + if page < 1: + if error_out: + abort(404) + else: + page = 1 + + if per_page < 1: + if error_out: + abort(404) + else: + per_page = 20 + + return page, per_page + + @property + def _query_offset(self) -> int: + """The index of the first item to query, passed to ``offset()``. + + :meta private: + + .. versionadded:: 3.0 + """ + return (self.page - 1) * self.per_page + + def _query_items(self) -> list[t.Any]: + """Execute the query to get the items on the current page. + + Uses init arguments stored in :attr:`_query_args`. + + :meta private: + + .. versionadded:: 3.0 + """ + raise NotImplementedError + + def _query_count(self) -> int: + """Execute the query to get the total number of items. + + Uses init arguments stored in :attr:`_query_args`. + + :meta private: + + .. versionadded:: 3.0 + """ + raise NotImplementedError + + @property + def first(self) -> int: + """The number of the first item on the page, starting from 1, or 0 if there are + no items. + + .. versionadded:: 3.0 + """ + if len(self.items) == 0: + return 0 + + return (self.page - 1) * self.per_page + 1 + + @property + def last(self) -> int: + """The number of the last item on the page, starting from 1, inclusive, or 0 if + there are no items. + + .. versionadded:: 3.0 + """ + first = self.first + return max(first, first + len(self.items) - 1) + + @property + def pages(self) -> int: + """The total number of pages.""" + if self.total == 0 or self.total is None: + return 0 + + return ceil(self.total / self.per_page) + + @property + def has_prev(self) -> bool: + """``True`` if this is not the first page.""" + return self.page > 1 + + @property + def prev_num(self) -> int | None: + """The previous page number, or ``None`` if this is the first page.""" + if not self.has_prev: + return None + + return self.page - 1 + + def prev(self, *, error_out: bool = False) -> Pagination: + """Query the :class:`Pagination` object for the previous page. + + :param error_out: Abort with a ``404 Not Found`` error if no items are returned + and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if + either are not ints. + """ + p = type(self)( + page=self.page - 1, + per_page=self.per_page, + error_out=error_out, + count=False, + **self._query_args, + ) + p.total = self.total + return p + + @property + def has_next(self) -> bool: + """``True`` if this is not the last page.""" + return self.page < self.pages + + @property + def next_num(self) -> int | None: + """The next page number, or ``None`` if this is the last page.""" + if not self.has_next: + return None + + return self.page + 1 + + def next(self, *, error_out: bool = False) -> Pagination: + """Query the :class:`Pagination` object for the next page. + + :param error_out: Abort with a ``404 Not Found`` error if no items are returned + and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if + either are not ints. + """ + p = type(self)( + page=self.page + 1, + per_page=self.per_page, + max_per_page=self.max_per_page, + error_out=error_out, + count=False, + **self._query_args, + ) + p.total = self.total + return p + + def iter_pages( + self, + *, + left_edge: int = 2, + left_current: int = 2, + right_current: int = 4, + right_edge: int = 2, + ) -> t.Iterator[int | None]: + """Yield page numbers for a pagination widget. Skipped pages between the edges + and middle are represented by a ``None``. + + For example, if there are 20 pages and the current page is 7, the following + values are yielded. + + .. code-block:: python + + 1, 2, None, 5, 6, 7, 8, 9, 10, 11, None, 19, 20 + + :param left_edge: How many pages to show from the first page. + :param left_current: How many pages to show left of the current page. + :param right_current: How many pages to show right of the current page. + :param right_edge: How many pages to show from the last page. + + .. versionchanged:: 3.0 + Improved efficiency of calculating what to yield. + + .. versionchanged:: 3.0 + ``right_current`` boundary is inclusive. + + .. versionchanged:: 3.0 + All parameters are keyword-only. + """ + pages_end = self.pages + 1 + + if pages_end == 1: + return + + left_end = min(1 + left_edge, pages_end) + yield from range(1, left_end) + + if left_end == pages_end: + return + + mid_start = max(left_end, self.page - left_current) + mid_end = min(self.page + right_current + 1, pages_end) + + if mid_start - left_end > 0: + yield None + + yield from range(mid_start, mid_end) + + if mid_end == pages_end: + return + + right_start = max(mid_end, pages_end - right_edge) + + if right_start - mid_end > 0: + yield None + + yield from range(right_start, pages_end) + + def __iter__(self) -> t.Iterator[t.Any]: + yield from self.items + + +class SelectPagination(Pagination): + """Returned by :meth:`.SQLAlchemy.paginate`. Takes ``select`` and ``session`` + arguments in addition to the :class:`Pagination` arguments. + + .. versionadded:: 3.0 + """ + + def _query_items(self) -> list[t.Any]: + select = self._query_args["select"] + select = select.limit(self.per_page).offset(self._query_offset) + session = self._query_args["session"] + return list(session.execute(select).unique().scalars()) + + def _query_count(self) -> int: + select = self._query_args["select"] + sub = select.options(sa_orm.lazyload("*")).order_by(None).subquery() + session = self._query_args["session"] + out = session.execute(sa.select(sa.func.count()).select_from(sub)).scalar() + return out # type: ignore[no-any-return] + + +class QueryPagination(Pagination): + """Returned by :meth:`.Query.paginate`. Takes a ``query`` argument in addition to + the :class:`Pagination` arguments. + + .. versionadded:: 3.0 + """ + + def _query_items(self) -> list[t.Any]: + query = self._query_args["query"] + out = query.limit(self.per_page).offset(self._query_offset).all() + return out # type: ignore[no-any-return] + + def _query_count(self) -> int: + # Query.count automatically disables eager loads + out = self._query_args["query"].order_by(None).count() + return out # type: ignore[no-any-return] diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/py.typed b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/query.py b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/query.py new file mode 100644 index 0000000..35f927d --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/query.py @@ -0,0 +1,105 @@ +from __future__ import annotations + +import typing as t + +import sqlalchemy.exc as sa_exc +import sqlalchemy.orm as sa_orm +from flask import abort + +from .pagination import Pagination +from .pagination import QueryPagination + + +class Query(sa_orm.Query): # type: ignore[type-arg] + """SQLAlchemy :class:`~sqlalchemy.orm.query.Query` subclass with some extra methods + useful for querying in a web application. + + This is the default query class for :attr:`.Model.query`. + + .. versionchanged:: 3.0 + Renamed to ``Query`` from ``BaseQuery``. + """ + + def get_or_404(self, ident: t.Any, description: str | None = None) -> t.Any: + """Like :meth:`~sqlalchemy.orm.Query.get` but aborts with a ``404 Not Found`` + error instead of returning ``None``. + + :param ident: The primary key to query. + :param description: A custom message to show on the error page. + """ + rv = self.get(ident) + + if rv is None: + abort(404, description=description) + + return rv + + def first_or_404(self, description: str | None = None) -> t.Any: + """Like :meth:`~sqlalchemy.orm.Query.first` but aborts with a ``404 Not Found`` + error instead of returning ``None``. + + :param description: A custom message to show on the error page. + """ + rv = self.first() + + if rv is None: + abort(404, description=description) + + return rv + + def one_or_404(self, description: str | None = None) -> t.Any: + """Like :meth:`~sqlalchemy.orm.Query.one` but aborts with a ``404 Not Found`` + error instead of raising ``NoResultFound`` or ``MultipleResultsFound``. + + :param description: A custom message to show on the error page. + + .. versionadded:: 3.0 + """ + try: + return self.one() + except (sa_exc.NoResultFound, sa_exc.MultipleResultsFound): + abort(404, description=description) + + def paginate( + self, + *, + page: int | None = None, + per_page: int | None = None, + max_per_page: int | None = None, + error_out: bool = True, + count: bool = True, + ) -> Pagination: + """Apply an offset and limit to the query based on the current page and number + of items per page, returning a :class:`.Pagination` object. + + :param page: The current page, used to calculate the offset. Defaults to the + ``page`` query arg during a request, or 1 otherwise. + :param per_page: The maximum number of items on a page, used to calculate the + offset and limit. Defaults to the ``per_page`` query arg during a request, + or 20 otherwise. + :param max_per_page: The maximum allowed value for ``per_page``, to limit a + user-provided value. Use ``None`` for no limit. Defaults to 100. + :param error_out: Abort with a ``404 Not Found`` error if no items are returned + and ``page`` is not 1, or if ``page`` or ``per_page`` is less than 1, or if + either are not ints. + :param count: Calculate the total number of values by issuing an extra count + query. For very complex queries this may be inaccurate or slow, so it can be + disabled and set manually if necessary. + + .. versionchanged:: 3.0 + All parameters are keyword-only. + + .. versionchanged:: 3.0 + The ``count`` query is more efficient. + + .. versionchanged:: 3.0 + ``max_per_page`` defaults to 100. + """ + return QueryPagination( + query=self, + page=page, + per_page=per_page, + max_per_page=max_per_page, + error_out=error_out, + count=count, + ) diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/record_queries.py b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/record_queries.py new file mode 100644 index 0000000..e8273be --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/record_queries.py @@ -0,0 +1,117 @@ +from __future__ import annotations + +import dataclasses +import inspect +import typing as t +from time import perf_counter + +import sqlalchemy as sa +import sqlalchemy.event as sa_event +from flask import current_app +from flask import g +from flask import has_app_context + + +def get_recorded_queries() -> list[_QueryInfo]: + """Get the list of recorded query information for the current session. Queries are + recorded if the config :data:`.SQLALCHEMY_RECORD_QUERIES` is enabled. + + Each query info object has the following attributes: + + ``statement`` + The string of SQL generated by SQLAlchemy with parameter placeholders. + ``parameters`` + The parameters sent with the SQL statement. + ``start_time`` / ``end_time`` + Timing info about when the query started execution and when the results where + returned. Accuracy and value depends on the operating system. + ``duration`` + The time the query took in seconds. + ``location`` + A string description of where in your application code the query was executed. + This may not be possible to calculate, and the format is not stable. + + .. versionchanged:: 3.0 + Renamed from ``get_debug_queries``. + + .. versionchanged:: 3.0 + The info object is a dataclass instead of a tuple. + + .. versionchanged:: 3.0 + The info object attribute ``context`` is renamed to ``location``. + + .. versionchanged:: 3.0 + Not enabled automatically in debug or testing mode. + """ + return g.get("_sqlalchemy_queries", []) # type: ignore[no-any-return] + + +@dataclasses.dataclass +class _QueryInfo: + """Information about an executed query. Returned by :func:`get_recorded_queries`. + + .. versionchanged:: 3.0 + Renamed from ``_DebugQueryTuple``. + + .. versionchanged:: 3.0 + Changed to a dataclass instead of a tuple. + + .. versionchanged:: 3.0 + ``context`` is renamed to ``location``. + """ + + statement: str | None + parameters: t.Any + start_time: float + end_time: float + location: str + + @property + def duration(self) -> float: + return self.end_time - self.start_time + + +def _listen(engine: sa.engine.Engine) -> None: + sa_event.listen(engine, "before_cursor_execute", _record_start, named=True) + sa_event.listen(engine, "after_cursor_execute", _record_end, named=True) + + +def _record_start(context: sa.engine.ExecutionContext, **kwargs: t.Any) -> None: + if not has_app_context(): + return + + context._fsa_start_time = perf_counter() # type: ignore[attr-defined] + + +def _record_end(context: sa.engine.ExecutionContext, **kwargs: t.Any) -> None: + if not has_app_context(): + return + + if "_sqlalchemy_queries" not in g: + g._sqlalchemy_queries = [] + + import_top = current_app.import_name.partition(".")[0] + import_dot = f"{import_top}." + frame = inspect.currentframe() + + while frame: + name = frame.f_globals.get("__name__") + + if name and (name == import_top or name.startswith(import_dot)): + code = frame.f_code + location = f"{code.co_filename}:{frame.f_lineno} ({code.co_name})" + break + + frame = frame.f_back + else: + location = "" + + g._sqlalchemy_queries.append( + _QueryInfo( + statement=context.statement, + parameters=context.parameters, + start_time=context._fsa_start_time, # type: ignore[attr-defined] + end_time=perf_counter(), + location=location, + ) + ) diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/session.py b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/session.py new file mode 100644 index 0000000..631fffa --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/session.py @@ -0,0 +1,111 @@ +from __future__ import annotations + +import typing as t + +import sqlalchemy as sa +import sqlalchemy.exc as sa_exc +import sqlalchemy.orm as sa_orm +from flask.globals import app_ctx + +if t.TYPE_CHECKING: + from .extension import SQLAlchemy + + +class Session(sa_orm.Session): + """A SQLAlchemy :class:`~sqlalchemy.orm.Session` class that chooses what engine to + use based on the bind key associated with the metadata associated with the thing + being queried. + + To customize ``db.session``, subclass this and pass it as the ``class_`` key in the + ``session_options`` to :class:`.SQLAlchemy`. + + .. versionchanged:: 3.0 + Renamed from ``SignallingSession``. + """ + + def __init__(self, db: SQLAlchemy, **kwargs: t.Any) -> None: + super().__init__(**kwargs) + self._db = db + self._model_changes: dict[object, tuple[t.Any, str]] = {} + + def get_bind( + self, + mapper: t.Any | None = None, + clause: t.Any | None = None, + bind: sa.engine.Engine | sa.engine.Connection | None = None, + **kwargs: t.Any, + ) -> sa.engine.Engine | sa.engine.Connection: + """Select an engine based on the ``bind_key`` of the metadata associated with + the model or table being queried. If no bind key is set, uses the default bind. + + .. versionchanged:: 3.0.3 + Fix finding the bind for a joined inheritance model. + + .. versionchanged:: 3.0 + The implementation more closely matches the base SQLAlchemy implementation. + + .. versionchanged:: 2.1 + Support joining an external transaction. + """ + if bind is not None: + return bind + + engines = self._db.engines + + if mapper is not None: + try: + mapper = sa.inspect(mapper) + except sa_exc.NoInspectionAvailable as e: + if isinstance(mapper, type): + raise sa_orm.exc.UnmappedClassError(mapper) from e + + raise + + engine = _clause_to_engine(mapper.local_table, engines) + + if engine is not None: + return engine + + if clause is not None: + engine = _clause_to_engine(clause, engines) + + if engine is not None: + return engine + + if None in engines: + return engines[None] + + return super().get_bind(mapper=mapper, clause=clause, bind=bind, **kwargs) + + +def _clause_to_engine( + clause: sa.ClauseElement | None, + engines: t.Mapping[str | None, sa.engine.Engine], +) -> sa.engine.Engine | None: + """If the clause is a table, return the engine associated with the table's + metadata's bind key. + """ + table = None + + if clause is not None: + if isinstance(clause, sa.Table): + table = clause + elif isinstance(clause, sa.UpdateBase) and isinstance(clause.table, sa.Table): + table = clause.table + + if table is not None and "bind_key" in table.metadata.info: + key = table.metadata.info["bind_key"] + + if key not in engines: + raise sa_exc.UnboundExecutionError( + f"Bind key '{key}' is not in 'SQLALCHEMY_BINDS' config." + ) + + return engines[key] + + return None + + +def _app_ctx_id() -> int: + """Get the id of the current Flask application context for the session scope.""" + return id(app_ctx._get_current_object()) # type: ignore[attr-defined] diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/table.py b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/table.py new file mode 100644 index 0000000..ab08a69 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/table.py @@ -0,0 +1,39 @@ +from __future__ import annotations + +import typing as t + +import sqlalchemy as sa +import sqlalchemy.sql.schema as sa_sql_schema + + +class _Table(sa.Table): + @t.overload + def __init__( + self, + name: str, + *args: sa_sql_schema.SchemaItem, + bind_key: str | None = None, + **kwargs: t.Any, + ) -> None: + ... + + @t.overload + def __init__( + self, + name: str, + metadata: sa.MetaData, + *args: sa_sql_schema.SchemaItem, + **kwargs: t.Any, + ) -> None: + ... + + @t.overload + def __init__( + self, name: str, *args: sa_sql_schema.SchemaItem, **kwargs: t.Any + ) -> None: + ... + + def __init__( + self, name: str, *args: sa_sql_schema.SchemaItem, **kwargs: t.Any + ) -> None: + super().__init__(name, *args, **kwargs) # type: ignore[arg-type] diff --git a/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/track_modifications.py b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/track_modifications.py new file mode 100644 index 0000000..7028b65 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/flask_sqlalchemy/track_modifications.py @@ -0,0 +1,88 @@ +from __future__ import annotations + +import typing as t + +import sqlalchemy as sa +import sqlalchemy.event as sa_event +import sqlalchemy.orm as sa_orm +from flask import current_app +from flask import has_app_context +from flask.signals import Namespace # type: ignore[attr-defined] + +if t.TYPE_CHECKING: + from .session import Session + +_signals = Namespace() + +models_committed = _signals.signal("models-committed") +"""This Blinker signal is sent after the session is committed if there were changed +models in the session. + +The sender is the application that emitted the changes. The receiver is passed the +``changes`` argument with a list of tuples in the form ``(instance, operation)``. +The operations are ``"insert"``, ``"update"``, and ``"delete"``. +""" + +before_models_committed = _signals.signal("before-models-committed") +"""This signal works exactly like :data:`models_committed` but is emitted before the +commit takes place. +""" + + +def _listen(session: sa_orm.scoped_session[Session]) -> None: + sa_event.listen(session, "before_flush", _record_ops, named=True) + sa_event.listen(session, "before_commit", _record_ops, named=True) + sa_event.listen(session, "before_commit", _before_commit) + sa_event.listen(session, "after_commit", _after_commit) + sa_event.listen(session, "after_rollback", _after_rollback) + + +def _record_ops(session: Session, **kwargs: t.Any) -> None: + if not has_app_context(): + return + + if not current_app.config["SQLALCHEMY_TRACK_MODIFICATIONS"]: + return + + for targets, operation in ( + (session.new, "insert"), + (session.dirty, "update"), + (session.deleted, "delete"), + ): + for target in targets: + state = sa.inspect(target) + key = state.identity_key if state.has_identity else id(target) + session._model_changes[key] = (target, operation) + + +def _before_commit(session: Session) -> None: + if not has_app_context(): + return + + app = current_app._get_current_object() # type: ignore[attr-defined] + + if not app.config["SQLALCHEMY_TRACK_MODIFICATIONS"]: + return + + if session._model_changes: + changes = list(session._model_changes.values()) + before_models_committed.send(app, changes=changes) + + +def _after_commit(session: Session) -> None: + if not has_app_context(): + return + + app = current_app._get_current_object() # type: ignore[attr-defined] + + if not app.config["SQLALCHEMY_TRACK_MODIFICATIONS"]: + return + + if session._model_changes: + changes = list(session._model_changes.values()) + models_committed.send(app, changes=changes) + session._model_changes.clear() + + +def _after_rollback(session: Session) -> None: + session._model_changes.clear() diff --git a/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/METADATA new file mode 100644 index 0000000..0e3a649 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/RECORD new file mode 100644 index 0000000..2f1fe6e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/WHEEL new file mode 100644 index 0000000..283ae68 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE b/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE new file mode 100644 index 0000000..b73a4a1 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE.PSF b/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/licenses/LICENSE.PSF new file mode 100644 index 0000000..d3b509a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/top_level.txt b/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/top_level.txt new file mode 100644 index 0000000..46725be --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/greenlet-3.2.4.dist-info/top_level.txt @@ -0,0 +1 @@ +greenlet diff --git a/tapdown/lib/python3.11/site-packages/greenlet/CObjects.cpp b/tapdown/lib/python3.11/site-packages/greenlet/CObjects.cpp new file mode 100644 index 0000000..c135995 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/PyGreenlet.cpp b/tapdown/lib/python3.11/site-packages/greenlet/PyGreenlet.cpp new file mode 100644 index 0000000..6b118a5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/PyGreenlet.hpp b/tapdown/lib/python3.11/site-packages/greenlet/PyGreenlet.hpp new file mode 100644 index 0000000..df6cd80 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/PyGreenletUnswitchable.cpp b/tapdown/lib/python3.11/site-packages/greenlet/PyGreenletUnswitchable.cpp new file mode 100644 index 0000000..1b768ee --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/PyModule.cpp b/tapdown/lib/python3.11/site-packages/greenlet/PyModule.cpp new file mode 100644 index 0000000..6adcb5c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TBrokenGreenlet.cpp b/tapdown/lib/python3.11/site-packages/greenlet/TBrokenGreenlet.cpp new file mode 100644 index 0000000..7e9ab5b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TExceptionState.cpp b/tapdown/lib/python3.11/site-packages/greenlet/TExceptionState.cpp new file mode 100644 index 0000000..08a94ae --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TGreenlet.cpp b/tapdown/lib/python3.11/site-packages/greenlet/TGreenlet.cpp new file mode 100644 index 0000000..d12722b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TGreenlet.hpp b/tapdown/lib/python3.11/site-packages/greenlet/TGreenlet.hpp new file mode 100644 index 0000000..e152353 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TGreenletGlobals.cpp b/tapdown/lib/python3.11/site-packages/greenlet/TGreenletGlobals.cpp new file mode 100644 index 0000000..0087d2f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TMainGreenlet.cpp b/tapdown/lib/python3.11/site-packages/greenlet/TMainGreenlet.cpp new file mode 100644 index 0000000..a2a9cfe --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TPythonState.cpp b/tapdown/lib/python3.11/site-packages/greenlet/TPythonState.cpp new file mode 100644 index 0000000..8833a80 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TStackState.cpp b/tapdown/lib/python3.11/site-packages/greenlet/TStackState.cpp new file mode 100644 index 0000000..9743ab5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TThreadState.hpp b/tapdown/lib/python3.11/site-packages/greenlet/TThreadState.hpp new file mode 100644 index 0000000..e4e6f6c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TThreadStateCreator.hpp b/tapdown/lib/python3.11/site-packages/greenlet/TThreadStateCreator.hpp new file mode 100644 index 0000000..2ec7ab5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TThreadStateDestroy.cpp b/tapdown/lib/python3.11/site-packages/greenlet/TThreadStateDestroy.cpp new file mode 100644 index 0000000..449b788 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/TUserGreenlet.cpp b/tapdown/lib/python3.11/site-packages/greenlet/TUserGreenlet.cpp new file mode 100644 index 0000000..73a8133 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/__init__.py b/tapdown/lib/python3.11/site-packages/greenlet/__init__.py new file mode 100644 index 0000000..6401497 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/_greenlet.cpython-311-x86_64-linux-gnu.so b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/workers/__init__.py b/tapdown/lib/python3.11/site-packages/gunicorn/workers/__init__.py new file mode 100644 index 0000000..3da5f85 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/workers/base.py b/tapdown/lib/python3.11/site-packages/gunicorn/workers/base.py new file mode 100644 index 0000000..93c465c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/workers/base_async.py b/tapdown/lib/python3.11/site-packages/gunicorn/workers/base_async.py new file mode 100644 index 0000000..9466d6a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/workers/geventlet.py b/tapdown/lib/python3.11/site-packages/gunicorn/workers/geventlet.py new file mode 100644 index 0000000..087eb61 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/workers/ggevent.py b/tapdown/lib/python3.11/site-packages/gunicorn/workers/ggevent.py new file mode 100644 index 0000000..b9b9b44 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/workers/gthread.py b/tapdown/lib/python3.11/site-packages/gunicorn/workers/gthread.py new file mode 100644 index 0000000..7a23228 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/workers/gtornado.py b/tapdown/lib/python3.11/site-packages/gunicorn/workers/gtornado.py new file mode 100644 index 0000000..544af7d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/workers/sync.py b/tapdown/lib/python3.11/site-packages/gunicorn/workers/sync.py new file mode 100644 index 0000000..4c029f9 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/workers/workertmp.py b/tapdown/lib/python3.11/site-packages/gunicorn/workers/workertmp.py new file mode 100644 index 0000000..8ef00a5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/METADATA new file mode 100644 index 0000000..8a2f639 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/METADATA @@ -0,0 +1,202 @@ +Metadata-Version: 2.4 +Name: h11 +Version: 0.16.0 +Summary: A pure-Python, bring-your-own-I/O implementation of HTTP/1.1 +Home-page: https://github.com/python-hyper/h11 +Author: Nathaniel J. Smith +Author-email: njs@pobox.com +License: MIT +Classifier: Development Status :: 3 - Alpha +Classifier: Intended Audience :: Developers +Classifier: License :: OSI Approved :: MIT License +Classifier: Programming Language :: Python :: Implementation :: CPython +Classifier: Programming Language :: Python :: Implementation :: PyPy +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3 :: Only +Classifier: Programming Language :: Python :: 3.8 +Classifier: Programming Language :: Python :: 3.9 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Programming Language :: Python :: 3.11 +Classifier: Programming Language :: Python :: 3.12 +Classifier: Topic :: Internet :: WWW/HTTP +Classifier: Topic :: System :: Networking +Requires-Python: >=3.8 +License-File: LICENSE.txt +Dynamic: author +Dynamic: author-email +Dynamic: classifier +Dynamic: description +Dynamic: home-page +Dynamic: license +Dynamic: license-file +Dynamic: requires-python +Dynamic: summary + +h11 +=== + +.. image:: https://travis-ci.org/python-hyper/h11.svg?branch=master + :target: https://travis-ci.org/python-hyper/h11 + :alt: Automated test status + +.. image:: https://codecov.io/gh/python-hyper/h11/branch/master/graph/badge.svg + :target: https://codecov.io/gh/python-hyper/h11 + :alt: Test coverage + +.. image:: https://readthedocs.org/projects/h11/badge/?version=latest + :target: http://h11.readthedocs.io/en/latest/?badge=latest + :alt: Documentation Status + +This is a little HTTP/1.1 library written from scratch in Python, +heavily inspired by `hyper-h2 `_. + +It's a "bring-your-own-I/O" library; h11 contains no IO code +whatsoever. This means you can hook h11 up to your favorite network +API, and that could be anything you want: synchronous, threaded, +asynchronous, or your own implementation of `RFC 6214 +`_ -- h11 won't judge you. +(Compare this to the current state of the art, where every time a `new +network API `_ comes along then someone +gets to start over reimplementing the entire HTTP protocol from +scratch.) Cory Benfield made an `excellent blog post describing the +benefits of this approach +`_, or if you like video +then here's his `PyCon 2016 talk on the same theme +`_. + +This also means that h11 is not immediately useful out of the box: +it's a toolkit for building programs that speak HTTP, not something +that could directly replace ``requests`` or ``twisted.web`` or +whatever. But h11 makes it much easier to implement something like +``requests`` or ``twisted.web``. + +At a high level, working with h11 goes like this: + +1) First, create an ``h11.Connection`` object to track the state of a + single HTTP/1.1 connection. + +2) When you read data off the network, pass it to + ``conn.receive_data(...)``; you'll get back a list of objects + representing high-level HTTP "events". + +3) When you want to send a high-level HTTP event, create the + corresponding "event" object and pass it to ``conn.send(...)``; + this will give you back some bytes that you can then push out + through the network. + +For example, a client might instantiate and then send a +``h11.Request`` object, then zero or more ``h11.Data`` objects for the +request body (e.g., if this is a POST), and then a +``h11.EndOfMessage`` to indicate the end of the message. Then the +server would then send back a ``h11.Response``, some ``h11.Data``, and +its own ``h11.EndOfMessage``. If either side violates the protocol, +you'll get a ``h11.ProtocolError`` exception. + +h11 is suitable for implementing both servers and clients, and has a +pleasantly symmetric API: the events you send as a client are exactly +the ones that you receive as a server and vice-versa. + +`Here's an example of a tiny HTTP client +`_ + +It also has `a fine manual `_. + +FAQ +--- + +*Whyyyyy?* + +I wanted to play with HTTP in `Curio +`__ and `Trio +`__, which at the time didn't have any +HTTP libraries. So I thought, no big deal, Python has, like, a dozen +different implementations of HTTP, surely I can find one that's +reusable. I didn't find one, but I did find Cory's call-to-arms +blog-post. So I figured, well, fine, if I have to implement HTTP from +scratch, at least I can make sure no-one *else* has to ever again. + +*Should I use it?* + +Maybe. You should be aware that it's a very young project. But, it's +feature complete and has an exhaustive test-suite and complete docs, +so the next step is for people to try using it and see how it goes +:-). If you do then please let us know -- if nothing else we'll want +to talk to you before making any incompatible changes! + +*What are the features/limitations?* + +Roughly speaking, it's trying to be a robust, complete, and non-hacky +implementation of the first "chapter" of the HTTP/1.1 spec: `RFC 7230: +HTTP/1.1 Message Syntax and Routing +`_. That is, it mostly focuses on +implementing HTTP at the level of taking bytes on and off the wire, +and the headers related to that, and tries to be anal about spec +conformance. It doesn't know about higher-level concerns like URL +routing, conditional GETs, cross-origin cookie policies, or content +negotiation. But it does know how to take care of framing, +cross-version differences in keep-alive handling, and the "obsolete +line folding" rule, so you can focus your energies on the hard / +interesting parts for your application, and it tries to support the +full specification in the sense that any useful HTTP/1.1 conformant +application should be able to use h11. + +It's pure Python, and has no dependencies outside of the standard +library. + +It has a test suite with 100.0% coverage for both statements and +branches. + +Currently it supports Python 3 (testing on 3.8-3.12) and PyPy 3. +The last Python 2-compatible version was h11 0.11.x. +(Originally it had a Cython wrapper for `http-parser +`_ and a beautiful nested state +machine implemented with ``yield from`` to postprocess the output. But +I had to take these out -- the new *parser* needs fewer lines-of-code +than the old *parser wrapper*, is written in pure Python, uses no +exotic language syntax, and has more features. It's sad, really; that +old state machine was really slick. I just need a few sentences here +to mourn that.) + +I don't know how fast it is. I haven't benchmarked or profiled it yet, +so it's probably got a few pointless hot spots, and I've been trying +to err on the side of simplicity and robustness instead of +micro-optimization. But at the architectural level I tried hard to +avoid fundamentally bad decisions, e.g., I believe that all the +parsing algorithms remain linear-time even in the face of pathological +input like slowloris, and there are no byte-by-byte loops. (I also +believe that it maintains bounded memory usage in the face of +arbitrary/pathological input.) + +The whole library is ~800 lines-of-code. You can read and understand +the whole thing in less than an hour. Most of the energy invested in +this so far has been spent on trying to keep things simple by +minimizing special-cases and ad hoc state manipulation; even though it +is now quite small and simple, I'm still annoyed that I haven't +figured out how to make it even smaller and simpler. (Unfortunately, +HTTP does not lend itself to simplicity.) + +The API is ~feature complete and I don't expect the general outlines +to change much, but you can't judge an API's ergonomics until you +actually document and use it, so I'd expect some changes in the +details. + +*How do I try it?* + +.. code-block:: sh + + $ pip install h11 + $ git clone git@github.com:python-hyper/h11 + $ cd h11/examples + $ python basic-client.py + +and go from there. + +*License?* + +MIT + +*Code of conduct?* + +Contributors are requested to follow our `code of conduct +`_ in +all project spaces. diff --git a/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/RECORD new file mode 100644 index 0000000..8768040 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/RECORD @@ -0,0 +1,29 @@ +h11-0.16.0.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4 +h11-0.16.0.dist-info/METADATA,sha256=KPMmCYrAn8unm48YD5YIfIQf4kViFct7hyqcfVzRnWQ,8348 +h11-0.16.0.dist-info/RECORD,, +h11-0.16.0.dist-info/WHEEL,sha256=CmyFI0kx5cdEMTLiONQRbGQwjIoR1aIYB7eCAQ4KPJ0,91 +h11-0.16.0.dist-info/licenses/LICENSE.txt,sha256=N9tbuFkm2yikJ6JYZ_ELEjIAOuob5pzLhRE4rbjm82E,1124 +h11-0.16.0.dist-info/top_level.txt,sha256=F7dC4jl3zeh8TGHEPaWJrMbeuoWbS379Gwdi-Yvdcis,4 +h11/__init__.py,sha256=iO1KzkSO42yZ6ffg-VMgbx_ZVTWGUY00nRYEWn-s3kY,1507 +h11/__pycache__/__init__.cpython-311.pyc,, +h11/__pycache__/_abnf.cpython-311.pyc,, +h11/__pycache__/_connection.cpython-311.pyc,, +h11/__pycache__/_events.cpython-311.pyc,, +h11/__pycache__/_headers.cpython-311.pyc,, +h11/__pycache__/_readers.cpython-311.pyc,, +h11/__pycache__/_receivebuffer.cpython-311.pyc,, +h11/__pycache__/_state.cpython-311.pyc,, +h11/__pycache__/_util.cpython-311.pyc,, +h11/__pycache__/_version.cpython-311.pyc,, +h11/__pycache__/_writers.cpython-311.pyc,, +h11/_abnf.py,sha256=ybixr0xsupnkA6GFAyMubuXF6Tc1lb_hF890NgCsfNc,4815 +h11/_connection.py,sha256=k9YRVf6koZqbttBW36xSWaJpWdZwa-xQVU9AHEo9DuI,26863 +h11/_events.py,sha256=I97aXoal1Wu7dkL548BANBUCkOIbe-x5CioYA9IBY14,11792 +h11/_headers.py,sha256=P7D-lBNxHwdLZPLimmYwrPG-9ZkjElvvJZJdZAgSP-4,10412 +h11/_readers.py,sha256=a4RypORUCC3d0q_kxPuBIM7jTD8iLt5X91TH0FsduN4,8590 +h11/_receivebuffer.py,sha256=xrspsdsNgWFxRfQcTXxR8RrdjRXXTK0Io5cQYWpJ1Ws,5252 +h11/_state.py,sha256=_5LG_BGR8FCcFQeBPH-TMHgm_-B-EUcWCnQof_9XjFE,13231 +h11/_util.py,sha256=LWkkjXyJaFlAy6Lt39w73UStklFT5ovcvo0TkY7RYuk,4888 +h11/_version.py,sha256=GVSsbPSPDcOuF6ptfIiXnVJoaEm3ygXbMnqlr_Giahw,686 +h11/_writers.py,sha256=oFKm6PtjeHfbj4RLX7VB7KDc1gIY53gXG3_HR9ltmTA,5081 +h11/py.typed,sha256=sow9soTwP9T_gEAQSVh7Gb8855h04Nwmhs2We-JRgZM,7 diff --git a/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/WHEEL new file mode 100644 index 0000000..1eb3c49 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/WHEEL @@ -0,0 +1,5 @@ +Wheel-Version: 1.0 +Generator: setuptools (78.1.0) +Root-Is-Purelib: true +Tag: py3-none-any + diff --git a/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/licenses/LICENSE.txt b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..8f080ea --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/licenses/LICENSE.txt @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2016 Nathaniel J. Smith and other 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/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/top_level.txt b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/top_level.txt new file mode 100644 index 0000000..0d24def --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11-0.16.0.dist-info/top_level.txt @@ -0,0 +1 @@ +h11 diff --git a/tapdown/lib/python3.11/site-packages/h11/__init__.py b/tapdown/lib/python3.11/site-packages/h11/__init__.py new file mode 100644 index 0000000..989e92c --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/__init__.py @@ -0,0 +1,62 @@ +# A highish-level implementation of the HTTP/1.1 wire protocol (RFC 7230), +# containing no networking code at all, loosely modelled on hyper-h2's generic +# implementation of HTTP/2 (and in particular the h2.connection.H2Connection +# class). There's still a bunch of subtle details you need to get right if you +# want to make this actually useful, because it doesn't implement all the +# semantics to check that what you're asking to write to the wire is sensible, +# but at least it gets you out of dealing with the wire itself. + +from h11._connection import Connection, NEED_DATA, PAUSED +from h11._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from h11._state import ( + CLIENT, + CLOSED, + DONE, + ERROR, + IDLE, + MIGHT_SWITCH_PROTOCOL, + MUST_CLOSE, + SEND_BODY, + SEND_RESPONSE, + SERVER, + SWITCHED_PROTOCOL, +) +from h11._util import LocalProtocolError, ProtocolError, RemoteProtocolError +from h11._version import __version__ + +PRODUCT_ID = "python-h11/" + __version__ + + +__all__ = ( + "Connection", + "NEED_DATA", + "PAUSED", + "ConnectionClosed", + "Data", + "EndOfMessage", + "Event", + "InformationalResponse", + "Request", + "Response", + "CLIENT", + "CLOSED", + "DONE", + "ERROR", + "IDLE", + "MUST_CLOSE", + "SEND_BODY", + "SEND_RESPONSE", + "SERVER", + "SWITCHED_PROTOCOL", + "ProtocolError", + "LocalProtocolError", + "RemoteProtocolError", +) diff --git a/tapdown/lib/python3.11/site-packages/h11/_abnf.py b/tapdown/lib/python3.11/site-packages/h11/_abnf.py new file mode 100644 index 0000000..933587f --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/_abnf.py @@ -0,0 +1,132 @@ +# We use native strings for all the re patterns, to take advantage of string +# formatting, and then convert to bytestrings when compiling the final re +# objects. + +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#whitespace +# OWS = *( SP / HTAB ) +# ; optional whitespace +OWS = r"[ \t]*" + +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#rule.token.separators +# token = 1*tchar +# +# tchar = "!" / "#" / "$" / "%" / "&" / "'" / "*" +# / "+" / "-" / "." / "^" / "_" / "`" / "|" / "~" +# / DIGIT / ALPHA +# ; any VCHAR, except delimiters +token = r"[-!#$%&'*+.^_`|~0-9a-zA-Z]+" + +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#header.fields +# field-name = token +field_name = token + +# The standard says: +# +# 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 +# +# https://tools.ietf.org/html/rfc5234#appendix-B.1 +# +# VCHAR = %x21-7E +# ; visible (printing) characters +# +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#rule.quoted-string +# obs-text = %x80-FF +# +# However, the standard definition of field-content is WRONG! It disallows +# fields containing a single visible character surrounded by whitespace, +# e.g. "foo a bar". +# +# See: https://www.rfc-editor.org/errata_search.php?rfc=7230&eid=4189 +# +# So our definition of field_content attempts to fix it up... +# +# Also, we allow lots of control characters, because apparently people assume +# that they're legal in practice (e.g., google analytics makes cookies with +# \x01 in them!): +# https://github.com/python-hyper/h11/issues/57 +# We still don't allow NUL or whitespace, because those are often treated as +# meta-characters and letting them through can lead to nasty issues like SSRF. +vchar = r"[\x21-\x7e]" +vchar_or_obs_text = r"[^\x00\s]" +field_vchar = vchar_or_obs_text +field_content = r"{field_vchar}+(?:[ \t]+{field_vchar}+)*".format(**globals()) + +# We handle obs-fold at a different level, and our fixed-up field_content +# already grows to swallow the whole value, so ? instead of * +field_value = r"({field_content})?".format(**globals()) + +# header-field = field-name ":" OWS field-value OWS +header_field = ( + r"(?P{field_name})" + r":" + r"{OWS}" + r"(?P{field_value})" + r"{OWS}".format(**globals()) +) + +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#request.line +# +# request-line = method SP request-target SP HTTP-version CRLF +# method = token +# HTTP-version = HTTP-name "/" DIGIT "." DIGIT +# HTTP-name = %x48.54.54.50 ; "HTTP", case-sensitive +# +# request-target is complicated (see RFC 7230 sec 5.3) -- could be path, full +# URL, host+port (for connect), or even "*", but in any case we are guaranteed +# that it contists of the visible printing characters. +method = token +request_target = r"{vchar}+".format(**globals()) +http_version = r"HTTP/(?P[0-9]\.[0-9])" +request_line = ( + r"(?P{method})" + r" " + r"(?P{request_target})" + r" " + r"{http_version}".format(**globals()) +) + +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#status.line +# +# status-line = HTTP-version SP status-code SP reason-phrase CRLF +# status-code = 3DIGIT +# reason-phrase = *( HTAB / SP / VCHAR / obs-text ) +status_code = r"[0-9]{3}" +reason_phrase = r"([ \t]|{vchar_or_obs_text})*".format(**globals()) +status_line = ( + r"{http_version}" + r" " + r"(?P{status_code})" + # However, there are apparently a few too many servers out there that just + # leave out the reason phrase: + # https://github.com/scrapy/scrapy/issues/345#issuecomment-281756036 + # https://github.com/seanmonstar/httparse/issues/29 + # so make it optional. ?: is a non-capturing group. + r"(?: (?P{reason_phrase}))?".format(**globals()) +) + +HEXDIG = r"[0-9A-Fa-f]" +# Actually +# +# chunk-size = 1*HEXDIG +# +# but we impose an upper-limit to avoid ridiculosity. len(str(2**64)) == 20 +chunk_size = r"({HEXDIG}){{1,20}}".format(**globals()) +# Actually +# +# chunk-ext = *( ";" chunk-ext-name [ "=" chunk-ext-val ] ) +# +# but we aren't parsing the things so we don't really care. +chunk_ext = r";.*" +chunk_header = ( + r"(?P{chunk_size})" + r"(?P{chunk_ext})?" + r"{OWS}\r\n".format( + **globals() + ) # Even though the specification does not allow for extra whitespaces, + # we are lenient with trailing whitespaces because some servers on the wild use it. +) diff --git a/tapdown/lib/python3.11/site-packages/h11/_connection.py b/tapdown/lib/python3.11/site-packages/h11/_connection.py new file mode 100644 index 0000000..e37d82a --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/_connection.py @@ -0,0 +1,659 @@ +# This contains the main Connection class. Everything in h11 revolves around +# this. +from typing import ( + Any, + Callable, + cast, + Dict, + List, + Optional, + overload, + Tuple, + Type, + Union, +) + +from ._events import ( + ConnectionClosed, + Data, + EndOfMessage, + Event, + InformationalResponse, + Request, + Response, +) +from ._headers import get_comma_header, has_expect_100_continue, set_comma_header +from ._readers import READERS, ReadersType +from ._receivebuffer import ReceiveBuffer +from ._state import ( + _SWITCH_CONNECT, + _SWITCH_UPGRADE, + CLIENT, + ConnectionState, + DONE, + ERROR, + MIGHT_SWITCH_PROTOCOL, + SEND_BODY, + SERVER, + SWITCHED_PROTOCOL, +) +from ._util import ( # Import the internal things we need + LocalProtocolError, + RemoteProtocolError, + Sentinel, +) +from ._writers import WRITERS, WritersType + +# Everything in __all__ gets re-exported as part of the h11 public API. +__all__ = ["Connection", "NEED_DATA", "PAUSED"] + + +class NEED_DATA(Sentinel, metaclass=Sentinel): + pass + + +class PAUSED(Sentinel, metaclass=Sentinel): + pass + + +# If we ever have this much buffered without it making a complete parseable +# event, we error out. The only time we really buffer is when reading the +# request/response line + headers together, so this is effectively the limit on +# the size of that. +# +# Some precedents for defaults: +# - node.js: 80 * 1024 +# - tomcat: 8 * 1024 +# - IIS: 16 * 1024 +# - Apache: <8 KiB per line> +DEFAULT_MAX_INCOMPLETE_EVENT_SIZE = 16 * 1024 + + +# RFC 7230's rules for connection lifecycles: +# - If either side says they want to close the connection, then the connection +# must close. +# - HTTP/1.1 defaults to keep-alive unless someone says Connection: close +# - HTTP/1.0 defaults to close unless both sides say Connection: keep-alive +# (and even this is a mess -- e.g. if you're implementing a proxy then +# sending Connection: keep-alive is forbidden). +# +# We simplify life by simply not supporting keep-alive with HTTP/1.0 peers. So +# our rule is: +# - If someone says Connection: close, we will close +# - If someone uses HTTP/1.0, we will close. +def _keep_alive(event: Union[Request, Response]) -> bool: + connection = get_comma_header(event.headers, b"connection") + if b"close" in connection: + return False + if getattr(event, "http_version", b"1.1") < b"1.1": + return False + return True + + +def _body_framing( + request_method: bytes, event: Union[Request, Response] +) -> Tuple[str, Union[Tuple[()], Tuple[int]]]: + # Called when we enter SEND_BODY to figure out framing information for + # this body. + # + # These are the only two events that can trigger a SEND_BODY state: + assert type(event) in (Request, Response) + # Returns one of: + # + # ("content-length", count) + # ("chunked", ()) + # ("http/1.0", ()) + # + # which are (lookup key, *args) for constructing body reader/writer + # objects. + # + # Reference: https://tools.ietf.org/html/rfc7230#section-3.3.3 + # + # Step 1: some responses always have an empty body, regardless of what the + # headers say. + if type(event) is Response: + if ( + event.status_code in (204, 304) + or request_method == b"HEAD" + or (request_method == b"CONNECT" and 200 <= event.status_code < 300) + ): + return ("content-length", (0,)) + # Section 3.3.3 also lists another case -- responses with status_code + # < 200. For us these are InformationalResponses, not Responses, so + # they can't get into this function in the first place. + assert event.status_code >= 200 + + # Step 2: check for Transfer-Encoding (T-E beats C-L): + transfer_encodings = get_comma_header(event.headers, b"transfer-encoding") + if transfer_encodings: + assert transfer_encodings == [b"chunked"] + return ("chunked", ()) + + # Step 3: check for Content-Length + content_lengths = get_comma_header(event.headers, b"content-length") + if content_lengths: + return ("content-length", (int(content_lengths[0]),)) + + # Step 4: no applicable headers; fallback/default depends on type + if type(event) is Request: + return ("content-length", (0,)) + else: + return ("http/1.0", ()) + + +################################################################ +# +# The main Connection class +# +################################################################ + + +class Connection: + """An object encapsulating the state of an HTTP connection. + + Args: + our_role: If you're implementing a client, pass :data:`h11.CLIENT`. If + you're implementing a server, pass :data:`h11.SERVER`. + + max_incomplete_event_size (int): + The maximum number of bytes we're willing to buffer of an + incomplete event. In practice this mostly sets a limit on the + maximum size of the request/response line + headers. If this is + exceeded, then :meth:`next_event` will raise + :exc:`RemoteProtocolError`. + + """ + + def __init__( + self, + our_role: Type[Sentinel], + max_incomplete_event_size: int = DEFAULT_MAX_INCOMPLETE_EVENT_SIZE, + ) -> None: + self._max_incomplete_event_size = max_incomplete_event_size + # State and role tracking + if our_role not in (CLIENT, SERVER): + raise ValueError(f"expected CLIENT or SERVER, not {our_role!r}") + self.our_role = our_role + self.their_role: Type[Sentinel] + if our_role is CLIENT: + self.their_role = SERVER + else: + self.their_role = CLIENT + self._cstate = ConnectionState() + + # Callables for converting data->events or vice-versa given the + # current state + self._writer = self._get_io_object(self.our_role, None, WRITERS) + self._reader = self._get_io_object(self.their_role, None, READERS) + + # Holds any unprocessed received data + self._receive_buffer = ReceiveBuffer() + # If this is true, then it indicates that the incoming connection was + # closed *after* the end of whatever's in self._receive_buffer: + self._receive_buffer_closed = False + + # Extra bits of state that don't fit into the state machine. + # + # These two are only used to interpret framing headers for figuring + # out how to read/write response bodies. their_http_version is also + # made available as a convenient public API. + self.their_http_version: Optional[bytes] = None + self._request_method: Optional[bytes] = None + # This is pure flow-control and doesn't at all affect the set of legal + # transitions, so no need to bother ConnectionState with it: + self.client_is_waiting_for_100_continue = False + + @property + def states(self) -> Dict[Type[Sentinel], Type[Sentinel]]: + """A dictionary like:: + + {CLIENT: , SERVER: } + + See :ref:`state-machine` for details. + + """ + return dict(self._cstate.states) + + @property + def our_state(self) -> Type[Sentinel]: + """The current state of whichever role we are playing. See + :ref:`state-machine` for details. + """ + return self._cstate.states[self.our_role] + + @property + def their_state(self) -> Type[Sentinel]: + """The current state of whichever role we are NOT playing. See + :ref:`state-machine` for details. + """ + return self._cstate.states[self.their_role] + + @property + def they_are_waiting_for_100_continue(self) -> bool: + return self.their_role is CLIENT and self.client_is_waiting_for_100_continue + + def start_next_cycle(self) -> None: + """Attempt to reset our connection state for a new request/response + cycle. + + If both client and server are in :data:`DONE` state, then resets them + both to :data:`IDLE` state in preparation for a new request/response + cycle on this same connection. Otherwise, raises a + :exc:`LocalProtocolError`. + + See :ref:`keepalive-and-pipelining`. + + """ + old_states = dict(self._cstate.states) + self._cstate.start_next_cycle() + self._request_method = None + # self.their_http_version gets left alone, since it presumably lasts + # beyond a single request/response cycle + assert not self.client_is_waiting_for_100_continue + self._respond_to_state_changes(old_states) + + def _process_error(self, role: Type[Sentinel]) -> None: + old_states = dict(self._cstate.states) + self._cstate.process_error(role) + self._respond_to_state_changes(old_states) + + def _server_switch_event(self, event: Event) -> Optional[Type[Sentinel]]: + if type(event) is InformationalResponse and event.status_code == 101: + return _SWITCH_UPGRADE + if type(event) is Response: + if ( + _SWITCH_CONNECT in self._cstate.pending_switch_proposals + and 200 <= event.status_code < 300 + ): + return _SWITCH_CONNECT + return None + + # All events go through here + def _process_event(self, role: Type[Sentinel], event: Event) -> None: + # First, pass the event through the state machine to make sure it + # succeeds. + old_states = dict(self._cstate.states) + if role is CLIENT and type(event) is Request: + if event.method == b"CONNECT": + self._cstate.process_client_switch_proposal(_SWITCH_CONNECT) + if get_comma_header(event.headers, b"upgrade"): + self._cstate.process_client_switch_proposal(_SWITCH_UPGRADE) + server_switch_event = None + if role is SERVER: + server_switch_event = self._server_switch_event(event) + self._cstate.process_event(role, type(event), server_switch_event) + + # Then perform the updates triggered by it. + + if type(event) is Request: + self._request_method = event.method + + if role is self.their_role and type(event) in ( + Request, + Response, + InformationalResponse, + ): + event = cast(Union[Request, Response, InformationalResponse], event) + self.their_http_version = event.http_version + + # Keep alive handling + # + # RFC 7230 doesn't really say what one should do if Connection: close + # shows up on a 1xx InformationalResponse. I think the idea is that + # this is not supposed to happen. In any case, if it does happen, we + # ignore it. + if type(event) in (Request, Response) and not _keep_alive( + cast(Union[Request, Response], event) + ): + self._cstate.process_keep_alive_disabled() + + # 100-continue + if type(event) is Request and has_expect_100_continue(event): + self.client_is_waiting_for_100_continue = True + if type(event) in (InformationalResponse, Response): + self.client_is_waiting_for_100_continue = False + if role is CLIENT and type(event) in (Data, EndOfMessage): + self.client_is_waiting_for_100_continue = False + + self._respond_to_state_changes(old_states, event) + + def _get_io_object( + self, + role: Type[Sentinel], + event: Optional[Event], + io_dict: Union[ReadersType, WritersType], + ) -> Optional[Callable[..., Any]]: + # event may be None; it's only used when entering SEND_BODY + state = self._cstate.states[role] + if state is SEND_BODY: + # Special case: the io_dict has a dict of reader/writer factories + # that depend on the request/response framing. + framing_type, args = _body_framing( + cast(bytes, self._request_method), cast(Union[Request, Response], event) + ) + return io_dict[SEND_BODY][framing_type](*args) # type: ignore[index] + else: + # General case: the io_dict just has the appropriate reader/writer + # for this state + return io_dict.get((role, state)) # type: ignore[return-value] + + # This must be called after any action that might have caused + # self._cstate.states to change. + def _respond_to_state_changes( + self, + old_states: Dict[Type[Sentinel], Type[Sentinel]], + event: Optional[Event] = None, + ) -> None: + # Update reader/writer + if self.our_state != old_states[self.our_role]: + self._writer = self._get_io_object(self.our_role, event, WRITERS) + if self.their_state != old_states[self.their_role]: + self._reader = self._get_io_object(self.their_role, event, READERS) + + @property + def trailing_data(self) -> Tuple[bytes, bool]: + """Data that has been received, but not yet processed, represented as + a tuple with two elements, where the first is a byte-string containing + the unprocessed data itself, and the second is a bool that is True if + the receive connection was closed. + + See :ref:`switching-protocols` for discussion of why you'd want this. + """ + return (bytes(self._receive_buffer), self._receive_buffer_closed) + + def receive_data(self, data: bytes) -> None: + """Add data to our internal receive buffer. + + This does not actually do any processing on the data, just stores + it. To trigger processing, you have to call :meth:`next_event`. + + Args: + data (:term:`bytes-like object`): + The new data that was just received. + + Special case: If *data* is an empty byte-string like ``b""``, + then this indicates that the remote side has closed the + connection (end of file). Normally this is convenient, because + standard Python APIs like :meth:`file.read` or + :meth:`socket.recv` use ``b""`` to indicate end-of-file, while + other failures to read are indicated using other mechanisms + like raising :exc:`TimeoutError`. When using such an API you + can just blindly pass through whatever you get from ``read`` + to :meth:`receive_data`, and everything will work. + + But, if you have an API where reading an empty string is a + valid non-EOF condition, then you need to be aware of this and + make sure to check for such strings and avoid passing them to + :meth:`receive_data`. + + Returns: + Nothing, but after calling this you should call :meth:`next_event` + to parse the newly received data. + + Raises: + RuntimeError: + Raised if you pass an empty *data*, indicating EOF, and then + pass a non-empty *data*, indicating more data that somehow + arrived after the EOF. + + (Calling ``receive_data(b"")`` multiple times is fine, + and equivalent to calling it once.) + + """ + if data: + if self._receive_buffer_closed: + raise RuntimeError("received close, then received more data?") + self._receive_buffer += data + else: + self._receive_buffer_closed = True + + def _extract_next_receive_event( + self, + ) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: + state = self.their_state + # We don't pause immediately when they enter DONE, because even in + # DONE state we can still process a ConnectionClosed() event. But + # if we have data in our buffer, then we definitely aren't getting + # a ConnectionClosed() immediately and we need to pause. + if state is DONE and self._receive_buffer: + return PAUSED + if state is MIGHT_SWITCH_PROTOCOL or state is SWITCHED_PROTOCOL: + return PAUSED + assert self._reader is not None + event = self._reader(self._receive_buffer) + if event is None: + if not self._receive_buffer and self._receive_buffer_closed: + # In some unusual cases (basically just HTTP/1.0 bodies), EOF + # triggers an actual protocol event; in that case, we want to + # return that event, and then the state will change and we'll + # get called again to generate the actual ConnectionClosed(). + if hasattr(self._reader, "read_eof"): + event = self._reader.read_eof() + else: + event = ConnectionClosed() + if event is None: + event = NEED_DATA + return event # type: ignore[no-any-return] + + def next_event(self) -> Union[Event, Type[NEED_DATA], Type[PAUSED]]: + """Parse the next event out of our receive buffer, update our internal + state, and return it. + + This is a mutating operation -- think of it like calling :func:`next` + on an iterator. + + Returns: + : One of three things: + + 1) An event object -- see :ref:`events`. + + 2) The special constant :data:`NEED_DATA`, which indicates that + you need to read more data from your socket and pass it to + :meth:`receive_data` before this method will be able to return + any more events. + + 3) The special constant :data:`PAUSED`, which indicates that we + are not in a state where we can process incoming data (usually + because the peer has finished their part of the current + request/response cycle, and you have not yet called + :meth:`start_next_cycle`). See :ref:`flow-control` for details. + + Raises: + RemoteProtocolError: + The peer has misbehaved. You should close the connection + (possibly after sending some kind of 4xx response). + + Once this method returns :class:`ConnectionClosed` once, then all + subsequent calls will also return :class:`ConnectionClosed`. + + If this method raises any exception besides :exc:`RemoteProtocolError` + then that's a bug -- if it happens please file a bug report! + + If this method raises any exception then it also sets + :attr:`Connection.their_state` to :data:`ERROR` -- see + :ref:`error-handling` for discussion. + + """ + + if self.their_state is ERROR: + raise RemoteProtocolError("Can't receive data when peer state is ERROR") + try: + event = self._extract_next_receive_event() + if event not in [NEED_DATA, PAUSED]: + self._process_event(self.their_role, cast(Event, event)) + if event is NEED_DATA: + if len(self._receive_buffer) > self._max_incomplete_event_size: + # 431 is "Request header fields too large" which is pretty + # much the only situation where we can get here + raise RemoteProtocolError( + "Receive buffer too long", error_status_hint=431 + ) + if self._receive_buffer_closed: + # We're still trying to complete some event, but that's + # never going to happen because no more data is coming + raise RemoteProtocolError("peer unexpectedly closed connection") + return event + except BaseException as exc: + self._process_error(self.their_role) + if isinstance(exc, LocalProtocolError): + exc._reraise_as_remote_protocol_error() + else: + raise + + @overload + def send(self, event: ConnectionClosed) -> None: + ... + + @overload + def send( + self, event: Union[Request, InformationalResponse, Response, Data, EndOfMessage] + ) -> bytes: + ... + + @overload + def send(self, event: Event) -> Optional[bytes]: + ... + + def send(self, event: Event) -> Optional[bytes]: + """Convert a high-level event into bytes that can be sent to the peer, + while updating our internal state machine. + + Args: + event: The :ref:`event ` to send. + + Returns: + If ``type(event) is ConnectionClosed``, then returns + ``None``. Otherwise, returns a :term:`bytes-like object`. + + Raises: + LocalProtocolError: + Sending this event at this time would violate our + understanding of the HTTP/1.1 protocol. + + If this method raises any exception then it also sets + :attr:`Connection.our_state` to :data:`ERROR` -- see + :ref:`error-handling` for discussion. + + """ + data_list = self.send_with_data_passthrough(event) + if data_list is None: + return None + else: + return b"".join(data_list) + + def send_with_data_passthrough(self, event: Event) -> Optional[List[bytes]]: + """Identical to :meth:`send`, except that in situations where + :meth:`send` returns a single :term:`bytes-like object`, this instead + returns a list of them -- and when sending a :class:`Data` event, this + list is guaranteed to contain the exact object you passed in as + :attr:`Data.data`. See :ref:`sendfile` for discussion. + + """ + if self.our_state is ERROR: + raise LocalProtocolError("Can't send data when our state is ERROR") + try: + if type(event) is Response: + event = self._clean_up_response_headers_for_sending(event) + # We want to call _process_event before calling the writer, + # because if someone tries to do something invalid then this will + # give a sensible error message, while our writers all just assume + # they will only receive valid events. But, _process_event might + # change self._writer. So we have to do a little dance: + writer = self._writer + self._process_event(self.our_role, event) + if type(event) is ConnectionClosed: + return None + else: + # In any situation where writer is None, process_event should + # have raised ProtocolError + assert writer is not None + data_list: List[bytes] = [] + writer(event, data_list.append) + return data_list + except: + self._process_error(self.our_role) + raise + + def send_failed(self) -> None: + """Notify the state machine that we failed to send the data it gave + us. + + This causes :attr:`Connection.our_state` to immediately become + :data:`ERROR` -- see :ref:`error-handling` for discussion. + + """ + self._process_error(self.our_role) + + # When sending a Response, we take responsibility for a few things: + # + # - Sometimes you MUST set Connection: close. We take care of those + # times. (You can also set it yourself if you want, and if you do then + # we'll respect that and close the connection at the right time. But you + # don't have to worry about that unless you want to.) + # + # - The user has to set Content-Length if they want it. Otherwise, for + # responses that have bodies (e.g. not HEAD), then we will automatically + # select the right mechanism for streaming a body of unknown length, + # which depends on depending on the peer's HTTP version. + # + # This function's *only* responsibility is making sure headers are set up + # right -- everything downstream just looks at the headers. There are no + # side channels. + def _clean_up_response_headers_for_sending(self, response: Response) -> Response: + assert type(response) is Response + + headers = response.headers + need_close = False + + # HEAD requests need some special handling: they always act like they + # have Content-Length: 0, and that's how _body_framing treats + # them. But their headers are supposed to match what we would send if + # the request was a GET. (Technically there is one deviation allowed: + # we're allowed to leave out the framing headers -- see + # https://tools.ietf.org/html/rfc7231#section-4.3.2 . But it's just as + # easy to get them right.) + method_for_choosing_headers = cast(bytes, self._request_method) + if method_for_choosing_headers == b"HEAD": + method_for_choosing_headers = b"GET" + framing_type, _ = _body_framing(method_for_choosing_headers, response) + if framing_type in ("chunked", "http/1.0"): + # This response has a body of unknown length. + # If our peer is HTTP/1.1, we use Transfer-Encoding: chunked + # If our peer is HTTP/1.0, we use no framing headers, and close the + # connection afterwards. + # + # Make sure to clear Content-Length (in principle user could have + # set both and then we ignored Content-Length b/c + # Transfer-Encoding overwrote it -- this would be naughty of them, + # but the HTTP spec says that if our peer does this then we have + # to fix it instead of erroring out, so we'll accord the user the + # same respect). + headers = set_comma_header(headers, b"content-length", []) + if self.their_http_version is None or self.their_http_version < b"1.1": + # Either we never got a valid request and are sending back an + # error (their_http_version is None), so we assume the worst; + # or else we did get a valid HTTP/1.0 request, so we know that + # they don't understand chunked encoding. + headers = set_comma_header(headers, b"transfer-encoding", []) + # This is actually redundant ATM, since currently we + # unconditionally disable keep-alive when talking to HTTP/1.0 + # peers. But let's be defensive just in case we add + # Connection: keep-alive support later: + if self._request_method != b"HEAD": + need_close = True + else: + headers = set_comma_header(headers, b"transfer-encoding", [b"chunked"]) + + if not self._cstate.keep_alive or need_close: + # Make sure Connection: close is set + connection = set(get_comma_header(headers, b"connection")) + connection.discard(b"keep-alive") + connection.add(b"close") + headers = set_comma_header(headers, b"connection", sorted(connection)) + + return Response( + headers=headers, + status_code=response.status_code, + http_version=response.http_version, + reason=response.reason, + ) diff --git a/tapdown/lib/python3.11/site-packages/h11/_events.py b/tapdown/lib/python3.11/site-packages/h11/_events.py new file mode 100644 index 0000000..ca1c3ad --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/_events.py @@ -0,0 +1,369 @@ +# High level events that make up HTTP/1.1 conversations. Loosely inspired by +# the corresponding events in hyper-h2: +# +# http://python-hyper.org/h2/en/stable/api.html#events +# +# Don't subclass these. Stuff will break. + +import re +from abc import ABC +from dataclasses import dataclass +from typing import List, Tuple, Union + +from ._abnf import method, request_target +from ._headers import Headers, normalize_and_validate +from ._util import bytesify, LocalProtocolError, validate + +# Everything in __all__ gets re-exported as part of the h11 public API. +__all__ = [ + "Event", + "Request", + "InformationalResponse", + "Response", + "Data", + "EndOfMessage", + "ConnectionClosed", +] + +method_re = re.compile(method.encode("ascii")) +request_target_re = re.compile(request_target.encode("ascii")) + + +class Event(ABC): + """ + Base class for h11 events. + """ + + __slots__ = () + + +@dataclass(init=False, frozen=True) +class Request(Event): + """The beginning of an HTTP request. + + Fields: + + .. attribute:: method + + An HTTP method, e.g. ``b"GET"`` or ``b"POST"``. Always a byte + string. :term:`Bytes-like objects ` and native + strings containing only ascii characters will be automatically + converted to byte strings. + + .. attribute:: target + + The target of an HTTP request, e.g. ``b"/index.html"``, or one of the + more exotic formats described in `RFC 7320, section 5.3 + `_. Always a byte + string. :term:`Bytes-like objects ` and native + strings containing only ascii characters will be automatically + converted to byte strings. + + .. attribute:: headers + + Request headers, represented as a list of (name, value) pairs. See + :ref:`the header normalization rules ` for details. + + .. attribute:: http_version + + The HTTP protocol version, represented as a byte string like + ``b"1.1"``. See :ref:`the HTTP version normalization rules + ` for details. + + """ + + __slots__ = ("method", "headers", "target", "http_version") + + method: bytes + headers: Headers + target: bytes + http_version: bytes + + def __init__( + self, + *, + method: Union[bytes, str], + headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]], + target: Union[bytes, str], + http_version: Union[bytes, str] = b"1.1", + _parsed: bool = False, + ) -> None: + super().__init__() + if isinstance(headers, Headers): + object.__setattr__(self, "headers", headers) + else: + object.__setattr__( + self, "headers", normalize_and_validate(headers, _parsed=_parsed) + ) + if not _parsed: + object.__setattr__(self, "method", bytesify(method)) + object.__setattr__(self, "target", bytesify(target)) + object.__setattr__(self, "http_version", bytesify(http_version)) + else: + object.__setattr__(self, "method", method) + object.__setattr__(self, "target", target) + object.__setattr__(self, "http_version", http_version) + + # "A server MUST respond with a 400 (Bad Request) status code to any + # HTTP/1.1 request message that lacks a Host header field and to any + # request message that contains more than one Host header field or a + # Host header field with an invalid field-value." + # -- https://tools.ietf.org/html/rfc7230#section-5.4 + host_count = 0 + for name, value in self.headers: + if name == b"host": + host_count += 1 + if self.http_version == b"1.1" and host_count == 0: + raise LocalProtocolError("Missing mandatory Host: header") + if host_count > 1: + raise LocalProtocolError("Found multiple Host: headers") + + validate(method_re, self.method, "Illegal method characters") + validate(request_target_re, self.target, "Illegal target characters") + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(init=False, frozen=True) +class _ResponseBase(Event): + __slots__ = ("headers", "http_version", "reason", "status_code") + + headers: Headers + http_version: bytes + reason: bytes + status_code: int + + def __init__( + self, + *, + headers: Union[Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]]], + status_code: int, + http_version: Union[bytes, str] = b"1.1", + reason: Union[bytes, str] = b"", + _parsed: bool = False, + ) -> None: + super().__init__() + if isinstance(headers, Headers): + object.__setattr__(self, "headers", headers) + else: + object.__setattr__( + self, "headers", normalize_and_validate(headers, _parsed=_parsed) + ) + if not _parsed: + object.__setattr__(self, "reason", bytesify(reason)) + object.__setattr__(self, "http_version", bytesify(http_version)) + if not isinstance(status_code, int): + raise LocalProtocolError("status code must be integer") + # Because IntEnum objects are instances of int, but aren't + # duck-compatible (sigh), see gh-72. + object.__setattr__(self, "status_code", int(status_code)) + else: + object.__setattr__(self, "reason", reason) + object.__setattr__(self, "http_version", http_version) + object.__setattr__(self, "status_code", status_code) + + self.__post_init__() + + def __post_init__(self) -> None: + pass + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(init=False, frozen=True) +class InformationalResponse(_ResponseBase): + """An HTTP informational response. + + Fields: + + .. attribute:: status_code + + The status code of this response, as an integer. For an + :class:`InformationalResponse`, this is always in the range [100, + 200). + + .. attribute:: headers + + Request headers, represented as a list of (name, value) pairs. See + :ref:`the header normalization rules ` for + details. + + .. attribute:: http_version + + The HTTP protocol version, represented as a byte string like + ``b"1.1"``. See :ref:`the HTTP version normalization rules + ` for details. + + .. attribute:: reason + + The reason phrase of this response, as a byte string. For example: + ``b"OK"``, or ``b"Not Found"``. + + """ + + def __post_init__(self) -> None: + if not (100 <= self.status_code < 200): + raise LocalProtocolError( + "InformationalResponse status_code should be in range " + "[100, 200), not {}".format(self.status_code) + ) + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(init=False, frozen=True) +class Response(_ResponseBase): + """The beginning of an HTTP response. + + Fields: + + .. attribute:: status_code + + The status code of this response, as an integer. For an + :class:`Response`, this is always in the range [200, + 1000). + + .. attribute:: headers + + Request headers, represented as a list of (name, value) pairs. See + :ref:`the header normalization rules ` for details. + + .. attribute:: http_version + + The HTTP protocol version, represented as a byte string like + ``b"1.1"``. See :ref:`the HTTP version normalization rules + ` for details. + + .. attribute:: reason + + The reason phrase of this response, as a byte string. For example: + ``b"OK"``, or ``b"Not Found"``. + + """ + + def __post_init__(self) -> None: + if not (200 <= self.status_code < 1000): + raise LocalProtocolError( + "Response status_code should be in range [200, 1000), not {}".format( + self.status_code + ) + ) + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(init=False, frozen=True) +class Data(Event): + """Part of an HTTP message body. + + Fields: + + .. attribute:: data + + A :term:`bytes-like object` containing part of a message body. Or, if + using the ``combine=False`` argument to :meth:`Connection.send`, then + any object that your socket writing code knows what to do with, and for + which calling :func:`len` returns the number of bytes that will be + written -- see :ref:`sendfile` for details. + + .. attribute:: chunk_start + + A marker that indicates whether this data object is from the start of a + chunked transfer encoding chunk. This field is ignored when when a Data + event is provided to :meth:`Connection.send`: it is only valid on + events emitted from :meth:`Connection.next_event`. You probably + shouldn't use this attribute at all; see + :ref:`chunk-delimiters-are-bad` for details. + + .. attribute:: chunk_end + + A marker that indicates whether this data object is the last for a + given chunked transfer encoding chunk. This field is ignored when when + a Data event is provided to :meth:`Connection.send`: it is only valid + on events emitted from :meth:`Connection.next_event`. You probably + shouldn't use this attribute at all; see + :ref:`chunk-delimiters-are-bad` for details. + + """ + + __slots__ = ("data", "chunk_start", "chunk_end") + + data: bytes + chunk_start: bool + chunk_end: bool + + def __init__( + self, data: bytes, chunk_start: bool = False, chunk_end: bool = False + ) -> None: + object.__setattr__(self, "data", data) + object.__setattr__(self, "chunk_start", chunk_start) + object.__setattr__(self, "chunk_end", chunk_end) + + # This is an unhashable type. + __hash__ = None # type: ignore + + +# XX FIXME: "A recipient MUST ignore (or consider as an error) any fields that +# are forbidden to be sent in a trailer, since processing them as if they were +# present in the header section might bypass external security filters." +# https://svn.tools.ietf.org/svn/wg/httpbis/specs/rfc7230.html#chunked.trailer.part +# Unfortunately, the list of forbidden fields is long and vague :-/ +@dataclass(init=False, frozen=True) +class EndOfMessage(Event): + """The end of an HTTP message. + + Fields: + + .. attribute:: headers + + Default value: ``[]`` + + Any trailing headers attached to this message, represented as a list of + (name, value) pairs. See :ref:`the header normalization rules + ` for details. + + Must be empty unless ``Transfer-Encoding: chunked`` is in use. + + """ + + __slots__ = ("headers",) + + headers: Headers + + def __init__( + self, + *, + headers: Union[ + Headers, List[Tuple[bytes, bytes]], List[Tuple[str, str]], None + ] = None, + _parsed: bool = False, + ) -> None: + super().__init__() + if headers is None: + headers = Headers([]) + elif not isinstance(headers, Headers): + headers = normalize_and_validate(headers, _parsed=_parsed) + + object.__setattr__(self, "headers", headers) + + # This is an unhashable type. + __hash__ = None # type: ignore + + +@dataclass(frozen=True) +class ConnectionClosed(Event): + """This event indicates that the sender has closed their outgoing + connection. + + Note that this does not necessarily mean that they can't *receive* further + data, because TCP connections are composed to two one-way channels which + can be closed independently. See :ref:`closing` for details. + + No fields. + """ + + pass diff --git a/tapdown/lib/python3.11/site-packages/h11/_headers.py b/tapdown/lib/python3.11/site-packages/h11/_headers.py new file mode 100644 index 0000000..31da3e2 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/_headers.py @@ -0,0 +1,282 @@ +import re +from typing import AnyStr, cast, List, overload, Sequence, Tuple, TYPE_CHECKING, Union + +from ._abnf import field_name, field_value +from ._util import bytesify, LocalProtocolError, validate + +if TYPE_CHECKING: + from ._events import Request + +try: + from typing import Literal +except ImportError: + from typing_extensions import Literal # type: ignore + +CONTENT_LENGTH_MAX_DIGITS = 20 # allow up to 1 billion TB - 1 + + +# Facts +# ----- +# +# Headers are: +# keys: case-insensitive ascii +# values: mixture of ascii and raw bytes +# +# "Historically, HTTP has allowed field content with text in the ISO-8859-1 +# charset [ISO-8859-1], supporting other charsets only through use of +# [RFC2047] encoding. In practice, most HTTP header field values use only a +# subset of the US-ASCII charset [USASCII]. Newly defined header fields SHOULD +# limit their field values to US-ASCII octets. A recipient SHOULD treat other +# octets in field content (obs-text) as opaque data." +# And it deprecates all non-ascii values +# +# Leading/trailing whitespace in header names is forbidden +# +# Values get leading/trailing whitespace stripped +# +# Content-Disposition actually needs to contain unicode semantically; to +# accomplish this it has a terrifically weird way of encoding the filename +# itself as ascii (and even this still has lots of cross-browser +# incompatibilities) +# +# Order is important: +# "a proxy MUST NOT change the order of these field values when forwarding a +# message" +# (and there are several headers where the order indicates a preference) +# +# Multiple occurences of the same header: +# "A sender MUST NOT generate multiple header fields with the same field name +# in a message unless either the entire field value for that header field is +# defined as a comma-separated list [or the header is Set-Cookie which gets a +# special exception]" - RFC 7230. (cookies are in RFC 6265) +# +# So every header aside from Set-Cookie can be merged by b", ".join if it +# occurs repeatedly. But, of course, they can't necessarily be split by +# .split(b","), because quoting. +# +# Given all this mess (case insensitive, duplicates allowed, order is +# important, ...), there doesn't appear to be any standard way to handle +# headers in Python -- they're almost like dicts, but... actually just +# aren't. For now we punt and just use a super simple representation: headers +# are a list of pairs +# +# [(name1, value1), (name2, value2), ...] +# +# where all entries are bytestrings, names are lowercase and have no +# leading/trailing whitespace, and values are bytestrings with no +# leading/trailing whitespace. Searching and updating are done via naive O(n) +# methods. +# +# Maybe a dict-of-lists would be better? + +_content_length_re = re.compile(rb"[0-9]+") +_field_name_re = re.compile(field_name.encode("ascii")) +_field_value_re = re.compile(field_value.encode("ascii")) + + +class Headers(Sequence[Tuple[bytes, bytes]]): + """ + A list-like interface that allows iterating over headers as byte-pairs + of (lowercased-name, value). + + Internally we actually store the representation as three-tuples, + including both the raw original casing, in order to preserve casing + over-the-wire, and the lowercased name, for case-insensitive comparisions. + + r = Request( + method="GET", + target="/", + headers=[("Host", "example.org"), ("Connection", "keep-alive")], + http_version="1.1", + ) + assert r.headers == [ + (b"host", b"example.org"), + (b"connection", b"keep-alive") + ] + assert r.headers.raw_items() == [ + (b"Host", b"example.org"), + (b"Connection", b"keep-alive") + ] + """ + + __slots__ = "_full_items" + + def __init__(self, full_items: List[Tuple[bytes, bytes, bytes]]) -> None: + self._full_items = full_items + + def __bool__(self) -> bool: + return bool(self._full_items) + + def __eq__(self, other: object) -> bool: + return list(self) == list(other) # type: ignore + + def __len__(self) -> int: + return len(self._full_items) + + def __repr__(self) -> str: + return "" % repr(list(self)) + + def __getitem__(self, idx: int) -> Tuple[bytes, bytes]: # type: ignore[override] + _, name, value = self._full_items[idx] + return (name, value) + + def raw_items(self) -> List[Tuple[bytes, bytes]]: + return [(raw_name, value) for raw_name, _, value in self._full_items] + + +HeaderTypes = Union[ + List[Tuple[bytes, bytes]], + List[Tuple[bytes, str]], + List[Tuple[str, bytes]], + List[Tuple[str, str]], +] + + +@overload +def normalize_and_validate(headers: Headers, _parsed: Literal[True]) -> Headers: + ... + + +@overload +def normalize_and_validate(headers: HeaderTypes, _parsed: Literal[False]) -> Headers: + ... + + +@overload +def normalize_and_validate( + headers: Union[Headers, HeaderTypes], _parsed: bool = False +) -> Headers: + ... + + +def normalize_and_validate( + headers: Union[Headers, HeaderTypes], _parsed: bool = False +) -> Headers: + new_headers = [] + seen_content_length = None + saw_transfer_encoding = False + for name, value in headers: + # For headers coming out of the parser, we can safely skip some steps, + # because it always returns bytes and has already run these regexes + # over the data: + if not _parsed: + name = bytesify(name) + value = bytesify(value) + validate(_field_name_re, name, "Illegal header name {!r}", name) + validate(_field_value_re, value, "Illegal header value {!r}", value) + assert isinstance(name, bytes) + assert isinstance(value, bytes) + + raw_name = name + name = name.lower() + if name == b"content-length": + lengths = {length.strip() for length in value.split(b",")} + if len(lengths) != 1: + raise LocalProtocolError("conflicting Content-Length headers") + value = lengths.pop() + validate(_content_length_re, value, "bad Content-Length") + if len(value) > CONTENT_LENGTH_MAX_DIGITS: + raise LocalProtocolError("bad Content-Length") + if seen_content_length is None: + seen_content_length = value + new_headers.append((raw_name, name, value)) + elif seen_content_length != value: + raise LocalProtocolError("conflicting Content-Length headers") + elif name == b"transfer-encoding": + # "A server that receives a request message with a transfer coding + # it does not understand SHOULD respond with 501 (Not + # Implemented)." + # https://tools.ietf.org/html/rfc7230#section-3.3.1 + if saw_transfer_encoding: + raise LocalProtocolError( + "multiple Transfer-Encoding headers", error_status_hint=501 + ) + # "All transfer-coding names are case-insensitive" + # -- https://tools.ietf.org/html/rfc7230#section-4 + value = value.lower() + if value != b"chunked": + raise LocalProtocolError( + "Only Transfer-Encoding: chunked is supported", + error_status_hint=501, + ) + saw_transfer_encoding = True + new_headers.append((raw_name, name, value)) + else: + new_headers.append((raw_name, name, value)) + return Headers(new_headers) + + +def get_comma_header(headers: Headers, name: bytes) -> List[bytes]: + # Should only be used for headers whose value is a list of + # comma-separated, case-insensitive values. + # + # The header name `name` is expected to be lower-case bytes. + # + # Connection: meets these criteria (including cast insensitivity). + # + # Content-Length: technically is just a single value (1*DIGIT), but the + # standard makes reference to implementations that do multiple values, and + # using this doesn't hurt. Ditto, case insensitivity doesn't things either + # way. + # + # Transfer-Encoding: is more complex (allows for quoted strings), so + # splitting on , is actually wrong. For example, this is legal: + # + # Transfer-Encoding: foo; options="1,2", chunked + # + # and should be parsed as + # + # foo; options="1,2" + # chunked + # + # but this naive function will parse it as + # + # foo; options="1 + # 2" + # chunked + # + # However, this is okay because the only thing we are going to do with + # any Transfer-Encoding is reject ones that aren't just "chunked", so + # both of these will be treated the same anyway. + # + # Expect: the only legal value is the literal string + # "100-continue". Splitting on commas is harmless. Case insensitive. + # + out: List[bytes] = [] + for _, found_name, found_raw_value in headers._full_items: + if found_name == name: + found_raw_value = found_raw_value.lower() + for found_split_value in found_raw_value.split(b","): + found_split_value = found_split_value.strip() + if found_split_value: + out.append(found_split_value) + return out + + +def set_comma_header(headers: Headers, name: bytes, new_values: List[bytes]) -> Headers: + # The header name `name` is expected to be lower-case bytes. + # + # Note that when we store the header we use title casing for the header + # names, in order to match the conventional HTTP header style. + # + # Simply calling `.title()` is a blunt approach, but it's correct + # here given the cases where we're using `set_comma_header`... + # + # Connection, Content-Length, Transfer-Encoding. + new_headers: List[Tuple[bytes, bytes]] = [] + for found_raw_name, found_name, found_raw_value in headers._full_items: + if found_name != name: + new_headers.append((found_raw_name, found_raw_value)) + for new_value in new_values: + new_headers.append((name.title(), new_value)) + return normalize_and_validate(new_headers) + + +def has_expect_100_continue(request: "Request") -> bool: + # https://tools.ietf.org/html/rfc7231#section-5.1.1 + # "A server that receives a 100-continue expectation in an HTTP/1.0 request + # MUST ignore that expectation." + if request.http_version < b"1.1": + return False + expect = get_comma_header(request.headers, b"expect") + return b"100-continue" in expect diff --git a/tapdown/lib/python3.11/site-packages/h11/_readers.py b/tapdown/lib/python3.11/site-packages/h11/_readers.py new file mode 100644 index 0000000..576804c --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/_readers.py @@ -0,0 +1,250 @@ +# Code to read HTTP data +# +# Strategy: each reader is a callable which takes a ReceiveBuffer object, and +# either: +# 1) consumes some of it and returns an Event +# 2) raises a LocalProtocolError (for consistency -- e.g. we call validate() +# and it might raise a LocalProtocolError, so simpler just to always use +# this) +# 3) returns None, meaning "I need more data" +# +# If they have a .read_eof attribute, then this will be called if an EOF is +# received -- but this is optional. Either way, the actual ConnectionClosed +# event will be generated afterwards. +# +# READERS is a dict describing how to pick a reader. It maps states to either: +# - a reader +# - or, for body readers, a dict of per-framing reader factories + +import re +from typing import Any, Callable, Dict, Iterable, NoReturn, Optional, Tuple, Type, Union + +from ._abnf import chunk_header, header_field, request_line, status_line +from ._events import Data, EndOfMessage, InformationalResponse, Request, Response +from ._receivebuffer import ReceiveBuffer +from ._state import ( + CLIENT, + CLOSED, + DONE, + IDLE, + MUST_CLOSE, + SEND_BODY, + SEND_RESPONSE, + SERVER, +) +from ._util import LocalProtocolError, RemoteProtocolError, Sentinel, validate + +__all__ = ["READERS"] + +header_field_re = re.compile(header_field.encode("ascii")) +obs_fold_re = re.compile(rb"[ \t]+") + + +def _obsolete_line_fold(lines: Iterable[bytes]) -> Iterable[bytes]: + it = iter(lines) + last: Optional[bytes] = None + for line in it: + match = obs_fold_re.match(line) + if match: + if last is None: + raise LocalProtocolError("continuation line at start of headers") + if not isinstance(last, bytearray): + # Cast to a mutable type, avoiding copy on append to ensure O(n) time + last = bytearray(last) + last += b" " + last += line[match.end() :] + else: + if last is not None: + yield last + last = line + if last is not None: + yield last + + +def _decode_header_lines( + lines: Iterable[bytes], +) -> Iterable[Tuple[bytes, bytes]]: + for line in _obsolete_line_fold(lines): + matches = validate(header_field_re, line, "illegal header line: {!r}", line) + yield (matches["field_name"], matches["field_value"]) + + +request_line_re = re.compile(request_line.encode("ascii")) + + +def maybe_read_from_IDLE_client(buf: ReceiveBuffer) -> Optional[Request]: + lines = buf.maybe_extract_lines() + if lines is None: + if buf.is_next_line_obviously_invalid_request_line(): + raise LocalProtocolError("illegal request line") + return None + if not lines: + raise LocalProtocolError("no request line received") + matches = validate( + request_line_re, lines[0], "illegal request line: {!r}", lines[0] + ) + return Request( + headers=list(_decode_header_lines(lines[1:])), _parsed=True, **matches + ) + + +status_line_re = re.compile(status_line.encode("ascii")) + + +def maybe_read_from_SEND_RESPONSE_server( + buf: ReceiveBuffer, +) -> Union[InformationalResponse, Response, None]: + lines = buf.maybe_extract_lines() + if lines is None: + if buf.is_next_line_obviously_invalid_request_line(): + raise LocalProtocolError("illegal request line") + return None + if not lines: + raise LocalProtocolError("no response line received") + matches = validate(status_line_re, lines[0], "illegal status line: {!r}", lines[0]) + http_version = ( + b"1.1" if matches["http_version"] is None else matches["http_version"] + ) + reason = b"" if matches["reason"] is None else matches["reason"] + status_code = int(matches["status_code"]) + class_: Union[Type[InformationalResponse], Type[Response]] = ( + InformationalResponse if status_code < 200 else Response + ) + return class_( + headers=list(_decode_header_lines(lines[1:])), + _parsed=True, + status_code=status_code, + reason=reason, + http_version=http_version, + ) + + +class ContentLengthReader: + def __init__(self, length: int) -> None: + self._length = length + self._remaining = length + + def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: + if self._remaining == 0: + return EndOfMessage() + data = buf.maybe_extract_at_most(self._remaining) + if data is None: + return None + self._remaining -= len(data) + return Data(data=data) + + def read_eof(self) -> NoReturn: + raise RemoteProtocolError( + "peer closed connection without sending complete message body " + "(received {} bytes, expected {})".format( + self._length - self._remaining, self._length + ) + ) + + +chunk_header_re = re.compile(chunk_header.encode("ascii")) + + +class ChunkedReader: + def __init__(self) -> None: + self._bytes_in_chunk = 0 + # After reading a chunk, we have to throw away the trailing \r\n. + # This tracks the bytes that we need to match and throw away. + self._bytes_to_discard = b"" + self._reading_trailer = False + + def __call__(self, buf: ReceiveBuffer) -> Union[Data, EndOfMessage, None]: + if self._reading_trailer: + lines = buf.maybe_extract_lines() + if lines is None: + return None + return EndOfMessage(headers=list(_decode_header_lines(lines))) + if self._bytes_to_discard: + data = buf.maybe_extract_at_most(len(self._bytes_to_discard)) + if data is None: + return None + if data != self._bytes_to_discard[: len(data)]: + raise LocalProtocolError( + f"malformed chunk footer: {data!r} (expected {self._bytes_to_discard!r})" + ) + self._bytes_to_discard = self._bytes_to_discard[len(data) :] + if self._bytes_to_discard: + return None + # else, fall through and read some more + assert self._bytes_to_discard == b"" + if self._bytes_in_chunk == 0: + # We need to refill our chunk count + chunk_header = buf.maybe_extract_next_line() + if chunk_header is None: + return None + matches = validate( + chunk_header_re, + chunk_header, + "illegal chunk header: {!r}", + chunk_header, + ) + # XX FIXME: we discard chunk extensions. Does anyone care? + self._bytes_in_chunk = int(matches["chunk_size"], base=16) + if self._bytes_in_chunk == 0: + self._reading_trailer = True + return self(buf) + chunk_start = True + else: + chunk_start = False + assert self._bytes_in_chunk > 0 + data = buf.maybe_extract_at_most(self._bytes_in_chunk) + if data is None: + return None + self._bytes_in_chunk -= len(data) + if self._bytes_in_chunk == 0: + self._bytes_to_discard = b"\r\n" + chunk_end = True + else: + chunk_end = False + return Data(data=data, chunk_start=chunk_start, chunk_end=chunk_end) + + def read_eof(self) -> NoReturn: + raise RemoteProtocolError( + "peer closed connection without sending complete message body " + "(incomplete chunked read)" + ) + + +class Http10Reader: + def __call__(self, buf: ReceiveBuffer) -> Optional[Data]: + data = buf.maybe_extract_at_most(999999999) + if data is None: + return None + return Data(data=data) + + def read_eof(self) -> EndOfMessage: + return EndOfMessage() + + +def expect_nothing(buf: ReceiveBuffer) -> None: + if buf: + raise LocalProtocolError("Got data when expecting EOF") + return None + + +ReadersType = Dict[ + Union[Type[Sentinel], Tuple[Type[Sentinel], Type[Sentinel]]], + Union[Callable[..., Any], Dict[str, Callable[..., Any]]], +] + +READERS: ReadersType = { + (CLIENT, IDLE): maybe_read_from_IDLE_client, + (SERVER, IDLE): maybe_read_from_SEND_RESPONSE_server, + (SERVER, SEND_RESPONSE): maybe_read_from_SEND_RESPONSE_server, + (CLIENT, DONE): expect_nothing, + (CLIENT, MUST_CLOSE): expect_nothing, + (CLIENT, CLOSED): expect_nothing, + (SERVER, DONE): expect_nothing, + (SERVER, MUST_CLOSE): expect_nothing, + (SERVER, CLOSED): expect_nothing, + SEND_BODY: { + "chunked": ChunkedReader, + "content-length": ContentLengthReader, + "http/1.0": Http10Reader, + }, +} diff --git a/tapdown/lib/python3.11/site-packages/h11/_receivebuffer.py b/tapdown/lib/python3.11/site-packages/h11/_receivebuffer.py new file mode 100644 index 0000000..e5c4e08 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/_receivebuffer.py @@ -0,0 +1,153 @@ +import re +import sys +from typing import List, Optional, Union + +__all__ = ["ReceiveBuffer"] + + +# Operations we want to support: +# - find next \r\n or \r\n\r\n (\n or \n\n are also acceptable), +# or wait until there is one +# - read at-most-N bytes +# Goals: +# - on average, do this fast +# - worst case, do this in O(n) where n is the number of bytes processed +# Plan: +# - store bytearray, offset, how far we've searched for a separator token +# - use the how-far-we've-searched data to avoid rescanning +# - while doing a stream of uninterrupted processing, advance offset instead +# of constantly copying +# WARNING: +# - I haven't benchmarked or profiled any of this yet. +# +# Note that starting in Python 3.4, deleting the initial n bytes from a +# bytearray is amortized O(n), thanks to some excellent work by Antoine +# Martin: +# +# https://bugs.python.org/issue19087 +# +# This means that if we only supported 3.4+, we could get rid of the code here +# involving self._start and self.compress, because it's doing exactly the same +# thing that bytearray now does internally. +# +# BUT unfortunately, we still support 2.7, and reading short segments out of a +# long buffer MUST be O(bytes read) to avoid DoS issues, so we can't actually +# delete this code. Yet: +# +# https://pythonclock.org/ +# +# (Two things to double-check first though: make sure PyPy also has the +# optimization, and benchmark to make sure it's a win, since we do have a +# slightly clever thing where we delay calling compress() until we've +# processed a whole event, which could in theory be slightly more efficient +# than the internal bytearray support.) +blank_line_regex = re.compile(b"\n\r?\n", re.MULTILINE) + + +class ReceiveBuffer: + def __init__(self) -> None: + self._data = bytearray() + self._next_line_search = 0 + self._multiple_lines_search = 0 + + def __iadd__(self, byteslike: Union[bytes, bytearray]) -> "ReceiveBuffer": + self._data += byteslike + return self + + def __bool__(self) -> bool: + return bool(len(self)) + + def __len__(self) -> int: + return len(self._data) + + # for @property unprocessed_data + def __bytes__(self) -> bytes: + return bytes(self._data) + + def _extract(self, count: int) -> bytearray: + # extracting an initial slice of the data buffer and return it + out = self._data[:count] + del self._data[:count] + + self._next_line_search = 0 + self._multiple_lines_search = 0 + + return out + + def maybe_extract_at_most(self, count: int) -> Optional[bytearray]: + """ + Extract a fixed number of bytes from the buffer. + """ + out = self._data[:count] + if not out: + return None + + return self._extract(count) + + def maybe_extract_next_line(self) -> Optional[bytearray]: + """ + Extract the first line, if it is completed in the buffer. + """ + # Only search in buffer space that we've not already looked at. + search_start_index = max(0, self._next_line_search - 1) + partial_idx = self._data.find(b"\r\n", search_start_index) + + if partial_idx == -1: + self._next_line_search = len(self._data) + return None + + # + 2 is to compensate len(b"\r\n") + idx = partial_idx + 2 + + return self._extract(idx) + + def maybe_extract_lines(self) -> Optional[List[bytearray]]: + """ + Extract everything up to the first blank line, and return a list of lines. + """ + # Handle the case where we have an immediate empty line. + if self._data[:1] == b"\n": + self._extract(1) + return [] + + if self._data[:2] == b"\r\n": + self._extract(2) + return [] + + # Only search in buffer space that we've not already looked at. + match = blank_line_regex.search(self._data, self._multiple_lines_search) + if match is None: + self._multiple_lines_search = max(0, len(self._data) - 2) + return None + + # Truncate the buffer and return it. + idx = match.span(0)[-1] + out = self._extract(idx) + lines = out.split(b"\n") + + for line in lines: + if line.endswith(b"\r"): + del line[-1] + + assert lines[-2] == lines[-1] == b"" + + del lines[-2:] + + return lines + + # In theory we should wait until `\r\n` before starting to validate + # incoming data. However it's interesting to detect (very) invalid data + # early given they might not even contain `\r\n` at all (hence only + # timeout will get rid of them). + # This is not a 100% effective detection but more of a cheap sanity check + # allowing for early abort in some useful cases. + # This is especially interesting when peer is messing up with HTTPS and + # sent us a TLS stream where we were expecting plain HTTP given all + # versions of TLS so far start handshake with a 0x16 message type code. + def is_next_line_obviously_invalid_request_line(self) -> bool: + try: + # HTTP header line must not contain non-printable characters + # and should not start with a space + return self._data[0] < 0x21 + except IndexError: + return False diff --git a/tapdown/lib/python3.11/site-packages/h11/_state.py b/tapdown/lib/python3.11/site-packages/h11/_state.py new file mode 100644 index 0000000..3ad444b --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/_state.py @@ -0,0 +1,365 @@ +################################################################ +# The core state machine +################################################################ +# +# Rule 1: everything that affects the state machine and state transitions must +# live here in this file. As much as possible goes into the table-based +# representation, but for the bits that don't quite fit, the actual code and +# state must nonetheless live here. +# +# Rule 2: this file does not know about what role we're playing; it only knows +# about HTTP request/response cycles in the abstract. This ensures that we +# don't cheat and apply different rules to local and remote parties. +# +# +# Theory of operation +# =================== +# +# Possibly the simplest way to think about this is that we actually have 5 +# different state machines here. Yes, 5. These are: +# +# 1) The client state, with its complicated automaton (see the docs) +# 2) The server state, with its complicated automaton (see the docs) +# 3) The keep-alive state, with possible states {True, False} +# 4) The SWITCH_CONNECT state, with possible states {False, True} +# 5) The SWITCH_UPGRADE state, with possible states {False, True} +# +# For (3)-(5), the first state listed is the initial state. +# +# (1)-(3) are stored explicitly in member variables. The last +# two are stored implicitly in the pending_switch_proposals set as: +# (state of 4) == (_SWITCH_CONNECT in pending_switch_proposals) +# (state of 5) == (_SWITCH_UPGRADE in pending_switch_proposals) +# +# And each of these machines has two different kinds of transitions: +# +# a) Event-triggered +# b) State-triggered +# +# Event triggered is the obvious thing that you'd think it is: some event +# happens, and if it's the right event at the right time then a transition +# happens. But there are somewhat complicated rules for which machines can +# "see" which events. (As a rule of thumb, if a machine "sees" an event, this +# means two things: the event can affect the machine, and if the machine is +# not in a state where it expects that event then it's an error.) These rules +# are: +# +# 1) The client machine sees all h11.events objects emitted by the client. +# +# 2) The server machine sees all h11.events objects emitted by the server. +# +# It also sees the client's Request event. +# +# And sometimes, server events are annotated with a _SWITCH_* event. For +# example, we can have a (Response, _SWITCH_CONNECT) event, which is +# different from a regular Response event. +# +# 3) The keep-alive machine sees the process_keep_alive_disabled() event +# (which is derived from Request/Response events), and this event +# transitions it from True -> False, or from False -> False. There's no way +# to transition back. +# +# 4&5) The _SWITCH_* machines transition from False->True when we get a +# Request that proposes the relevant type of switch (via +# process_client_switch_proposals), and they go from True->False when we +# get a Response that has no _SWITCH_* annotation. +# +# So that's event-triggered transitions. +# +# State-triggered transitions are less standard. What they do here is couple +# the machines together. The way this works is, when certain *joint* +# configurations of states are achieved, then we automatically transition to a +# new *joint* state. So, for example, if we're ever in a joint state with +# +# client: DONE +# keep-alive: False +# +# then the client state immediately transitions to: +# +# client: MUST_CLOSE +# +# This is fundamentally different from an event-based transition, because it +# doesn't matter how we arrived at the {client: DONE, keep-alive: False} state +# -- maybe the client transitioned SEND_BODY -> DONE, or keep-alive +# transitioned True -> False. Either way, once this precondition is satisfied, +# this transition is immediately triggered. +# +# What if two conflicting state-based transitions get enabled at the same +# time? In practice there's only one case where this arises (client DONE -> +# MIGHT_SWITCH_PROTOCOL versus DONE -> MUST_CLOSE), and we resolve it by +# explicitly prioritizing the DONE -> MIGHT_SWITCH_PROTOCOL transition. +# +# Implementation +# -------------- +# +# The event-triggered transitions for the server and client machines are all +# stored explicitly in a table. Ditto for the state-triggered transitions that +# involve just the server and client state. +# +# The transitions for the other machines, and the state-triggered transitions +# that involve the other machines, are written out as explicit Python code. +# +# It'd be nice if there were some cleaner way to do all this. This isn't +# *too* terrible, but I feel like it could probably be better. +# +# WARNING +# ------- +# +# The script that generates the state machine diagrams for the docs knows how +# to read out the EVENT_TRIGGERED_TRANSITIONS and STATE_TRIGGERED_TRANSITIONS +# tables. But it can't automatically read the transitions that are written +# directly in Python code. So if you touch those, you need to also update the +# script to keep it in sync! +from typing import cast, Dict, Optional, Set, Tuple, Type, Union + +from ._events import * +from ._util import LocalProtocolError, Sentinel + +# Everything in __all__ gets re-exported as part of the h11 public API. +__all__ = [ + "CLIENT", + "SERVER", + "IDLE", + "SEND_RESPONSE", + "SEND_BODY", + "DONE", + "MUST_CLOSE", + "CLOSED", + "MIGHT_SWITCH_PROTOCOL", + "SWITCHED_PROTOCOL", + "ERROR", +] + + +class CLIENT(Sentinel, metaclass=Sentinel): + pass + + +class SERVER(Sentinel, metaclass=Sentinel): + pass + + +# States +class IDLE(Sentinel, metaclass=Sentinel): + pass + + +class SEND_RESPONSE(Sentinel, metaclass=Sentinel): + pass + + +class SEND_BODY(Sentinel, metaclass=Sentinel): + pass + + +class DONE(Sentinel, metaclass=Sentinel): + pass + + +class MUST_CLOSE(Sentinel, metaclass=Sentinel): + pass + + +class CLOSED(Sentinel, metaclass=Sentinel): + pass + + +class ERROR(Sentinel, metaclass=Sentinel): + pass + + +# Switch types +class MIGHT_SWITCH_PROTOCOL(Sentinel, metaclass=Sentinel): + pass + + +class SWITCHED_PROTOCOL(Sentinel, metaclass=Sentinel): + pass + + +class _SWITCH_UPGRADE(Sentinel, metaclass=Sentinel): + pass + + +class _SWITCH_CONNECT(Sentinel, metaclass=Sentinel): + pass + + +EventTransitionType = Dict[ + Type[Sentinel], + Dict[ + Type[Sentinel], + Dict[Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], Type[Sentinel]], + ], +] + +EVENT_TRIGGERED_TRANSITIONS: EventTransitionType = { + CLIENT: { + IDLE: {Request: SEND_BODY, ConnectionClosed: CLOSED}, + SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE}, + DONE: {ConnectionClosed: CLOSED}, + MUST_CLOSE: {ConnectionClosed: CLOSED}, + CLOSED: {ConnectionClosed: CLOSED}, + MIGHT_SWITCH_PROTOCOL: {}, + SWITCHED_PROTOCOL: {}, + ERROR: {}, + }, + SERVER: { + IDLE: { + ConnectionClosed: CLOSED, + Response: SEND_BODY, + # Special case: server sees client Request events, in this form + (Request, CLIENT): SEND_RESPONSE, + }, + SEND_RESPONSE: { + InformationalResponse: SEND_RESPONSE, + Response: SEND_BODY, + (InformationalResponse, _SWITCH_UPGRADE): SWITCHED_PROTOCOL, + (Response, _SWITCH_CONNECT): SWITCHED_PROTOCOL, + }, + SEND_BODY: {Data: SEND_BODY, EndOfMessage: DONE}, + DONE: {ConnectionClosed: CLOSED}, + MUST_CLOSE: {ConnectionClosed: CLOSED}, + CLOSED: {ConnectionClosed: CLOSED}, + SWITCHED_PROTOCOL: {}, + ERROR: {}, + }, +} + +StateTransitionType = Dict[ + Tuple[Type[Sentinel], Type[Sentinel]], Dict[Type[Sentinel], Type[Sentinel]] +] + +# NB: there are also some special-case state-triggered transitions hard-coded +# into _fire_state_triggered_transitions below. +STATE_TRIGGERED_TRANSITIONS: StateTransitionType = { + # (Client state, Server state) -> new states + # Protocol negotiation + (MIGHT_SWITCH_PROTOCOL, SWITCHED_PROTOCOL): {CLIENT: SWITCHED_PROTOCOL}, + # Socket shutdown + (CLOSED, DONE): {SERVER: MUST_CLOSE}, + (CLOSED, IDLE): {SERVER: MUST_CLOSE}, + (ERROR, DONE): {SERVER: MUST_CLOSE}, + (DONE, CLOSED): {CLIENT: MUST_CLOSE}, + (IDLE, CLOSED): {CLIENT: MUST_CLOSE}, + (DONE, ERROR): {CLIENT: MUST_CLOSE}, +} + + +class ConnectionState: + def __init__(self) -> None: + # Extra bits of state that don't quite fit into the state model. + + # If this is False then it enables the automatic DONE -> MUST_CLOSE + # transition. Don't set this directly; call .keep_alive_disabled() + self.keep_alive = True + + # This is a subset of {UPGRADE, CONNECT}, containing the proposals + # made by the client for switching protocols. + self.pending_switch_proposals: Set[Type[Sentinel]] = set() + + self.states: Dict[Type[Sentinel], Type[Sentinel]] = {CLIENT: IDLE, SERVER: IDLE} + + def process_error(self, role: Type[Sentinel]) -> None: + self.states[role] = ERROR + self._fire_state_triggered_transitions() + + def process_keep_alive_disabled(self) -> None: + self.keep_alive = False + self._fire_state_triggered_transitions() + + def process_client_switch_proposal(self, switch_event: Type[Sentinel]) -> None: + self.pending_switch_proposals.add(switch_event) + self._fire_state_triggered_transitions() + + def process_event( + self, + role: Type[Sentinel], + event_type: Type[Event], + server_switch_event: Optional[Type[Sentinel]] = None, + ) -> None: + _event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]] = event_type + if server_switch_event is not None: + assert role is SERVER + if server_switch_event not in self.pending_switch_proposals: + raise LocalProtocolError( + "Received server _SWITCH_UPGRADE event without a pending proposal" + ) + _event_type = (event_type, server_switch_event) + if server_switch_event is None and _event_type is Response: + self.pending_switch_proposals = set() + self._fire_event_triggered_transitions(role, _event_type) + # Special case: the server state does get to see Request + # events. + if _event_type is Request: + assert role is CLIENT + self._fire_event_triggered_transitions(SERVER, (Request, CLIENT)) + self._fire_state_triggered_transitions() + + def _fire_event_triggered_transitions( + self, + role: Type[Sentinel], + event_type: Union[Type[Event], Tuple[Type[Event], Type[Sentinel]]], + ) -> None: + state = self.states[role] + try: + new_state = EVENT_TRIGGERED_TRANSITIONS[role][state][event_type] + except KeyError: + event_type = cast(Type[Event], event_type) + raise LocalProtocolError( + "can't handle event type {} when role={} and state={}".format( + event_type.__name__, role, self.states[role] + ) + ) from None + self.states[role] = new_state + + def _fire_state_triggered_transitions(self) -> None: + # We apply these rules repeatedly until converging on a fixed point + while True: + start_states = dict(self.states) + + # It could happen that both these special-case transitions are + # enabled at the same time: + # + # DONE -> MIGHT_SWITCH_PROTOCOL + # DONE -> MUST_CLOSE + # + # For example, this will always be true of a HTTP/1.0 client + # requesting CONNECT. If this happens, the protocol switch takes + # priority. From there the client will either go to + # SWITCHED_PROTOCOL, in which case it's none of our business when + # they close the connection, or else the server will deny the + # request, in which case the client will go back to DONE and then + # from there to MUST_CLOSE. + if self.pending_switch_proposals: + if self.states[CLIENT] is DONE: + self.states[CLIENT] = MIGHT_SWITCH_PROTOCOL + + if not self.pending_switch_proposals: + if self.states[CLIENT] is MIGHT_SWITCH_PROTOCOL: + self.states[CLIENT] = DONE + + if not self.keep_alive: + for role in (CLIENT, SERVER): + if self.states[role] is DONE: + self.states[role] = MUST_CLOSE + + # Tabular state-triggered transitions + joint_state = (self.states[CLIENT], self.states[SERVER]) + changes = STATE_TRIGGERED_TRANSITIONS.get(joint_state, {}) + self.states.update(changes) + + if self.states == start_states: + # Fixed point reached + return + + def start_next_cycle(self) -> None: + if self.states != {CLIENT: DONE, SERVER: DONE}: + raise LocalProtocolError( + f"not in a reusable state. self.states={self.states}" + ) + # Can't reach DONE/DONE with any of these active, but still, let's be + # sure. + assert self.keep_alive + assert not self.pending_switch_proposals + self.states = {CLIENT: IDLE, SERVER: IDLE} diff --git a/tapdown/lib/python3.11/site-packages/h11/_util.py b/tapdown/lib/python3.11/site-packages/h11/_util.py new file mode 100644 index 0000000..6718445 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/_util.py @@ -0,0 +1,135 @@ +from typing import Any, Dict, NoReturn, Pattern, Tuple, Type, TypeVar, Union + +__all__ = [ + "ProtocolError", + "LocalProtocolError", + "RemoteProtocolError", + "validate", + "bytesify", +] + + +class ProtocolError(Exception): + """Exception indicating a violation of the HTTP/1.1 protocol. + + This as an abstract base class, with two concrete base classes: + :exc:`LocalProtocolError`, which indicates that you tried to do something + that HTTP/1.1 says is illegal, and :exc:`RemoteProtocolError`, which + indicates that the remote peer tried to do something that HTTP/1.1 says is + illegal. See :ref:`error-handling` for details. + + In addition to the normal :exc:`Exception` features, it has one attribute: + + .. attribute:: error_status_hint + + This gives a suggestion as to what status code a server might use if + this error occurred as part of a request. + + For a :exc:`RemoteProtocolError`, this is useful as a suggestion for + how you might want to respond to a misbehaving peer, if you're + implementing a server. + + For a :exc:`LocalProtocolError`, this can be taken as a suggestion for + how your peer might have responded to *you* if h11 had allowed you to + continue. + + The default is 400 Bad Request, a generic catch-all for protocol + violations. + + """ + + def __init__(self, msg: str, error_status_hint: int = 400) -> None: + if type(self) is ProtocolError: + raise TypeError("tried to directly instantiate ProtocolError") + Exception.__init__(self, msg) + self.error_status_hint = error_status_hint + + +# Strategy: there are a number of public APIs where a LocalProtocolError can +# be raised (send(), all the different event constructors, ...), and only one +# public API where RemoteProtocolError can be raised +# (receive_data()). Therefore we always raise LocalProtocolError internally, +# and then receive_data will translate this into a RemoteProtocolError. +# +# Internally: +# LocalProtocolError is the generic "ProtocolError". +# Externally: +# LocalProtocolError is for local errors and RemoteProtocolError is for +# remote errors. +class LocalProtocolError(ProtocolError): + def _reraise_as_remote_protocol_error(self) -> NoReturn: + # After catching a LocalProtocolError, use this method to re-raise it + # as a RemoteProtocolError. This method must be called from inside an + # except: block. + # + # An easy way to get an equivalent RemoteProtocolError is just to + # modify 'self' in place. + self.__class__ = RemoteProtocolError # type: ignore + # But the re-raising is somewhat non-trivial -- you might think that + # now that we've modified the in-flight exception object, that just + # doing 'raise' to re-raise it would be enough. But it turns out that + # this doesn't work, because Python tracks the exception type + # (exc_info[0]) separately from the exception object (exc_info[1]), + # and we only modified the latter. So we really do need to re-raise + # the new type explicitly. + # On py3, the traceback is part of the exception object, so our + # in-place modification preserved it and we can just re-raise: + raise self + + +class RemoteProtocolError(ProtocolError): + pass + + +def validate( + regex: Pattern[bytes], data: bytes, msg: str = "malformed data", *format_args: Any +) -> Dict[str, bytes]: + match = regex.fullmatch(data) + if not match: + if format_args: + msg = msg.format(*format_args) + raise LocalProtocolError(msg) + return match.groupdict() + + +# Sentinel values +# +# - Inherit identity-based comparison and hashing from object +# - Have a nice repr +# - Have a *bonus property*: type(sentinel) is sentinel +# +# The bonus property is useful if you want to take the return value from +# next_event() and do some sort of dispatch based on type(event). + +_T_Sentinel = TypeVar("_T_Sentinel", bound="Sentinel") + + +class Sentinel(type): + def __new__( + cls: Type[_T_Sentinel], + name: str, + bases: Tuple[type, ...], + namespace: Dict[str, Any], + **kwds: Any + ) -> _T_Sentinel: + assert bases == (Sentinel,) + v = super().__new__(cls, name, bases, namespace, **kwds) + v.__class__ = v # type: ignore + return v + + def __repr__(self) -> str: + return self.__name__ + + +# Used for methods, request targets, HTTP versions, header names, and header +# values. Accepts ascii-strings, or bytes/bytearray/memoryview/..., and always +# returns bytes. +def bytesify(s: Union[bytes, bytearray, memoryview, int, str]) -> bytes: + # Fast-path: + if type(s) is bytes: + return s + if isinstance(s, str): + s = s.encode("ascii") + if isinstance(s, int): + raise TypeError("expected bytes-like object, not int") + return bytes(s) diff --git a/tapdown/lib/python3.11/site-packages/h11/_version.py b/tapdown/lib/python3.11/site-packages/h11/_version.py new file mode 100644 index 0000000..76e7327 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/_version.py @@ -0,0 +1,16 @@ +# This file must be kept very simple, because it is consumed from several +# places -- it is imported by h11/__init__.py, execfile'd by setup.py, etc. + +# We use a simple scheme: +# 1.0.0 -> 1.0.0+dev -> 1.1.0 -> 1.1.0+dev +# where the +dev versions are never released into the wild, they're just what +# we stick into the VCS in between releases. +# +# This is compatible with PEP 440: +# http://legacy.python.org/dev/peps/pep-0440/ +# via the use of the "local suffix" "+dev", which is disallowed on index +# servers and causes 1.0.0+dev to sort after plain 1.0.0, which is what we +# want. (Contrast with the special suffix 1.0.0.dev, which sorts *before* +# 1.0.0.) + +__version__ = "0.16.0" diff --git a/tapdown/lib/python3.11/site-packages/h11/_writers.py b/tapdown/lib/python3.11/site-packages/h11/_writers.py new file mode 100644 index 0000000..939cdb9 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/_writers.py @@ -0,0 +1,145 @@ +# Code to read HTTP data +# +# Strategy: each writer takes an event + a write-some-bytes function, which is +# calls. +# +# WRITERS is a dict describing how to pick a reader. It maps states to either: +# - a writer +# - or, for body writers, a dict of framin-dependent writer factories + +from typing import Any, Callable, Dict, List, Tuple, Type, Union + +from ._events import Data, EndOfMessage, Event, InformationalResponse, Request, Response +from ._headers import Headers +from ._state import CLIENT, IDLE, SEND_BODY, SEND_RESPONSE, SERVER +from ._util import LocalProtocolError, Sentinel + +__all__ = ["WRITERS"] + +Writer = Callable[[bytes], Any] + + +def write_headers(headers: Headers, write: Writer) -> None: + # "Since the Host field-value is critical information for handling a + # request, a user agent SHOULD generate Host as the first header field + # following the request-line." - RFC 7230 + raw_items = headers._full_items + for raw_name, name, value in raw_items: + if name == b"host": + write(b"%s: %s\r\n" % (raw_name, value)) + for raw_name, name, value in raw_items: + if name != b"host": + write(b"%s: %s\r\n" % (raw_name, value)) + write(b"\r\n") + + +def write_request(request: Request, write: Writer) -> None: + if request.http_version != b"1.1": + raise LocalProtocolError("I only send HTTP/1.1") + write(b"%s %s HTTP/1.1\r\n" % (request.method, request.target)) + write_headers(request.headers, write) + + +# Shared between InformationalResponse and Response +def write_any_response( + response: Union[InformationalResponse, Response], write: Writer +) -> None: + if response.http_version != b"1.1": + raise LocalProtocolError("I only send HTTP/1.1") + status_bytes = str(response.status_code).encode("ascii") + # We don't bother sending ascii status messages like "OK"; they're + # optional and ignored by the protocol. (But the space after the numeric + # status code is mandatory.) + # + # XX FIXME: could at least make an effort to pull out the status message + # from stdlib's http.HTTPStatus table. Or maybe just steal their enums + # (either by import or copy/paste). We already accept them as status codes + # since they're of type IntEnum < int. + write(b"HTTP/1.1 %s %s\r\n" % (status_bytes, response.reason)) + write_headers(response.headers, write) + + +class BodyWriter: + def __call__(self, event: Event, write: Writer) -> None: + if type(event) is Data: + self.send_data(event.data, write) + elif type(event) is EndOfMessage: + self.send_eom(event.headers, write) + else: # pragma: no cover + assert False + + def send_data(self, data: bytes, write: Writer) -> None: + pass + + def send_eom(self, headers: Headers, write: Writer) -> None: + pass + + +# +# These are all careful not to do anything to 'data' except call len(data) and +# write(data). This allows us to transparently pass-through funny objects, +# like placeholder objects referring to files on disk that will be sent via +# sendfile(2). +# +class ContentLengthWriter(BodyWriter): + def __init__(self, length: int) -> None: + self._length = length + + def send_data(self, data: bytes, write: Writer) -> None: + self._length -= len(data) + if self._length < 0: + raise LocalProtocolError("Too much data for declared Content-Length") + write(data) + + def send_eom(self, headers: Headers, write: Writer) -> None: + if self._length != 0: + raise LocalProtocolError("Too little data for declared Content-Length") + if headers: + raise LocalProtocolError("Content-Length and trailers don't mix") + + +class ChunkedWriter(BodyWriter): + def send_data(self, data: bytes, write: Writer) -> None: + # if we encoded 0-length data in the naive way, it would look like an + # end-of-message. + if not data: + return + write(b"%x\r\n" % len(data)) + write(data) + write(b"\r\n") + + def send_eom(self, headers: Headers, write: Writer) -> None: + write(b"0\r\n") + write_headers(headers, write) + + +class Http10Writer(BodyWriter): + def send_data(self, data: bytes, write: Writer) -> None: + write(data) + + def send_eom(self, headers: Headers, write: Writer) -> None: + if headers: + raise LocalProtocolError("can't send trailers to HTTP/1.0 client") + # no need to close the socket ourselves, that will be taken care of by + # Connection: close machinery + + +WritersType = Dict[ + Union[Tuple[Type[Sentinel], Type[Sentinel]], Type[Sentinel]], + Union[ + Dict[str, Type[BodyWriter]], + Callable[[Union[InformationalResponse, Response], Writer], None], + Callable[[Request, Writer], None], + ], +] + +WRITERS: WritersType = { + (CLIENT, IDLE): write_request, + (SERVER, IDLE): write_any_response, + (SERVER, SEND_RESPONSE): write_any_response, + SEND_BODY: { + "chunked": ChunkedWriter, + "content-length": ContentLengthWriter, + "http/1.0": Http10Writer, + }, +} diff --git a/tapdown/lib/python3.11/site-packages/h11/py.typed b/tapdown/lib/python3.11/site-packages/h11/py.typed new file mode 100644 index 0000000..f5642f7 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/h11/py.typed @@ -0,0 +1 @@ +Marker diff --git a/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt b/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/LICENSE.txt new file mode 100644 index 0000000..7b190ca --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/METADATA new file mode 100644 index 0000000..ddf5464 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/RECORD new file mode 100644 index 0000000..333875c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/itsdangerous-2.2.0.dist-info/WHEEL new file mode 100644 index 0000000..3b5e64b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous/__init__.py b/tapdown/lib/python3.11/site-packages/itsdangerous/__init__.py new file mode 100644 index 0000000..ea55256 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous/_json.py b/tapdown/lib/python3.11/site-packages/itsdangerous/_json.py new file mode 100644 index 0000000..fc23fea --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous/encoding.py b/tapdown/lib/python3.11/site-packages/itsdangerous/encoding.py new file mode 100644 index 0000000..f5ca80f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous/exc.py b/tapdown/lib/python3.11/site-packages/itsdangerous/exc.py new file mode 100644 index 0000000..a75adcd --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous/py.typed b/tapdown/lib/python3.11/site-packages/itsdangerous/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/itsdangerous/serializer.py b/tapdown/lib/python3.11/site-packages/itsdangerous/serializer.py new file mode 100644 index 0000000..5ddf387 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous/signer.py b/tapdown/lib/python3.11/site-packages/itsdangerous/signer.py new file mode 100644 index 0000000..e324dc0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous/timed.py b/tapdown/lib/python3.11/site-packages/itsdangerous/timed.py new file mode 100644 index 0000000..7384375 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/itsdangerous/url_safe.py b/tapdown/lib/python3.11/site-packages/itsdangerous/url_safe.py new file mode 100644 index 0000000..56a0793 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/METADATA new file mode 100644 index 0000000..ffef2ff --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/RECORD new file mode 100644 index 0000000..4b48b91 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/WHEEL new file mode 100644 index 0000000..23d2d7e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/entry_points.txt b/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/entry_points.txt new file mode 100644 index 0000000..abc3eae --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt b/tapdown/lib/python3.11/site-packages/jinja2-3.1.6.dist-info/licenses/LICENSE.txt new file mode 100644 index 0000000..c37cae4 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2/__init__.py b/tapdown/lib/python3.11/site-packages/jinja2/__init__.py new file mode 100644 index 0000000..1a423a3 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2/_identifier.py b/tapdown/lib/python3.11/site-packages/jinja2/_identifier.py new file mode 100644 index 0000000..928c150 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2/async_utils.py b/tapdown/lib/python3.11/site-packages/jinja2/async_utils.py new file mode 100644 index 0000000..f0c1402 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2/bccache.py b/tapdown/lib/python3.11/site-packages/jinja2/bccache.py new file mode 100644 index 0000000..ada8b09 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2/compiler.py b/tapdown/lib/python3.11/site-packages/jinja2/compiler.py new file mode 100644 index 0000000..a4ff6a1 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2/constants.py b/tapdown/lib/python3.11/site-packages/jinja2/constants.py new file mode 100644 index 0000000..41a1c23 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2/debug.py b/tapdown/lib/python3.11/site-packages/jinja2/debug.py new file mode 100644 index 0000000..eeeeee7 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2/defaults.py b/tapdown/lib/python3.11/site-packages/jinja2/defaults.py new file mode 100644 index 0000000..638cad3 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/jinja2/environment.py b/tapdown/lib/python3.11/site-packages/jinja2/environment.py new file mode 100644 index 0000000..0fc6e5b --- /dev/null +++ b/tapdown/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 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 literal 0 HcmV?d00001 diff --git a/tapdown/lib/python3.11/site-packages/greenlet/greenlet.cpp b/tapdown/lib/python3.11/site-packages/greenlet/greenlet.cpp new file mode 100644 index 0000000..e8d92a0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/greenlet.h b/tapdown/lib/python3.11/site-packages/greenlet/greenlet.h new file mode 100644 index 0000000..d02a16e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/greenlet_allocator.hpp b/tapdown/lib/python3.11/site-packages/greenlet/greenlet_allocator.hpp new file mode 100644 index 0000000..dc2b969 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/greenlet_compiler_compat.hpp b/tapdown/lib/python3.11/site-packages/greenlet/greenlet_compiler_compat.hpp new file mode 100644 index 0000000..af24bd8 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/greenlet_cpython_compat.hpp b/tapdown/lib/python3.11/site-packages/greenlet/greenlet_cpython_compat.hpp new file mode 100644 index 0000000..a3b3850 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/greenlet_exceptions.hpp b/tapdown/lib/python3.11/site-packages/greenlet/greenlet_exceptions.hpp new file mode 100644 index 0000000..617f07c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/greenlet_internal.hpp b/tapdown/lib/python3.11/site-packages/greenlet/greenlet_internal.hpp new file mode 100644 index 0000000..f2b15d5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/greenlet_msvc_compat.hpp b/tapdown/lib/python3.11/site-packages/greenlet/greenlet_msvc_compat.hpp new file mode 100644 index 0000000..c00245b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/greenlet_refs.hpp b/tapdown/lib/python3.11/site-packages/greenlet/greenlet_refs.hpp new file mode 100644 index 0000000..b7e5e3f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/greenlet_slp_switch.hpp b/tapdown/lib/python3.11/site-packages/greenlet/greenlet_slp_switch.hpp new file mode 100644 index 0000000..bd4b7ae --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/greenlet_thread_support.hpp b/tapdown/lib/python3.11/site-packages/greenlet/greenlet_thread_support.hpp new file mode 100644 index 0000000..3ded7d2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/__init__.py b/tapdown/lib/python3.11/site-packages/greenlet/platform/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/greenlet/platform/setup_switch_x64_masm.cmd b/tapdown/lib/python3.11/site-packages/greenlet/platform/setup_switch_x64_masm.cmd new file mode 100644 index 0000000..0928595 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_aarch64_gcc.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_aarch64_gcc.h new file mode 100644 index 0000000..058617c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_alpha_unix.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_alpha_unix.h new file mode 100644 index 0000000..7e07abf --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_amd64_unix.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_amd64_unix.h new file mode 100644 index 0000000..d470110 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm32_gcc.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm32_gcc.h new file mode 100644 index 0000000..655003a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm32_ios.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm32_ios.h new file mode 100644 index 0000000..9e640e1 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.asm b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.asm new file mode 100644 index 0000000..29f9c22 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm64_masm.obj b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm64_msvc.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_arm64_msvc.h new file mode 100644 index 0000000..7ab7f45 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_csky_gcc.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_csky_gcc.h new file mode 100644 index 0000000..ac469d3 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_loongarch64_linux.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_loongarch64_linux.h new file mode 100644 index 0000000..9eaf34e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_m68k_gcc.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_m68k_gcc.h new file mode 100644 index 0000000..da761c2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_mips_unix.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_mips_unix.h new file mode 100644 index 0000000..b9003e9 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_aix.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_aix.h new file mode 100644 index 0000000..e7e0b87 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_linux.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc64_linux.h new file mode 100644 index 0000000..3c324d0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_aix.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_aix.h new file mode 100644 index 0000000..6d93c13 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_linux.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_linux.h new file mode 100644 index 0000000..e83ad70 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_macosx.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_macosx.h new file mode 100644 index 0000000..bd414c6 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_unix.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_ppc_unix.h new file mode 100644 index 0000000..bb18808 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_riscv_unix.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_riscv_unix.h new file mode 100644 index 0000000..8761122 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_s390_unix.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_s390_unix.h new file mode 100644 index 0000000..9199367 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_sh_gcc.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_sh_gcc.h new file mode 100644 index 0000000..5ecc3b3 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_sparc_sun_gcc.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_sparc_sun_gcc.h new file mode 100644 index 0000000..96990c3 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x32_unix.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x32_unix.h new file mode 100644 index 0000000..893369c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.asm b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.asm new file mode 100644 index 0000000..f5c72a2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x64_masm.obj b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x64_msvc.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x64_msvc.h new file mode 100644 index 0000000..601ea56 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x86_msvc.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x86_msvc.h new file mode 100644 index 0000000..0f3a59f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x86_unix.h b/tapdown/lib/python3.11/site-packages/greenlet/platform/switch_x86_unix.h new file mode 100644 index 0000000..493fa6b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/slp_platformselect.h b/tapdown/lib/python3.11/site-packages/greenlet/slp_platformselect.h new file mode 100644 index 0000000..d9b7d0a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/__init__.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/__init__.py new file mode 100644 index 0000000..1861360 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/_test_extension.c b/tapdown/lib/python3.11/site-packages/greenlet/tests/_test_extension.c new file mode 100644 index 0000000..05e81c0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/_test_extension.cpython-311-x86_64-linux-gnu.so b/tapdown/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 zcmeHPeQ;FQb-!9rA|osb+fZz9c-SH%?68)walkh9>ifwNAS2=8kj&GQ_DR|z?J95I zg5+cp*)j}kl|&_JLp}Ker!$Sm(`0DI@z6}0k--Jr$uz3dx{TY5HErTqMc9FOAg=4O z{hfR7S-pMUrJjCF`^R2Jd+zz2k9+R9@B7}%S6VyULSza6P`%CXLMX4sFB^Awwu#-OlNhnm{usaGDdQ>l^hhHwF(wYx!JB?cu}fH z?o6B@W>l04kwEWKNPQlzhL=&0EP4~6v1CDf>#x3_o*WKr;6ZzTm=6@5&ToY4g3X58-P24Ao$}VcN#GM|yZ`e_?!V;^0 z%(ez%aeFxS1Qff*tro`~9!~U&uJKmawYnXz+jC>_LGV4}qmI>)>`wK0s7d9WXan+8 zzo+$^>}aDqm>O~75*h2a8k0%K^)4*N89=i=nk1xSWHjM= zVmQ_}>J32}4JQ-fdgA-3cE4!v>}YDXHitKdw;;D=*ZmiQR29;T_>Q5}ZLu5ZYZP5Wtx~5>f53{^N$?PXzEe%`;{FnAXn(^k?Yef%0BN62bbX z)+=5#=Hn?t^8s9FKdS?HjXs!-0DeUKsSDusH!J^J1Nd>R9|_<;(6|}Edv8*H_6G31 zI-cGD{)+Z96u_tTxQqqx*R}pc0Kcg5BLTc#$8$V@AJ_4m2;k-GRs7Efa9jjrITgUy zY5kc1zD?uj1NiJp<>x{GpV9Hm1@IQ_ClkOU+Ru9d{F3%_@?q5u-E+!RRsNpx;e78T z!Hf?l8!qR4cqN0Nvp)PrAAZ4ySNrfeAD(yrh$y9xgM@tg6+WEqv0Q2*sz&LT#t9zR4rwo zlz~zPN*VaS%fP$UpLpM#yjWpQmH*j2LYRkUyij)5oP4F?j0`Hf<*xx}H~bjC)$1at z5kEw-`ME60hSS8;Ep~oZ@;@Y=Ze{Z`lK(#ObPJn5Dft(Or(5s*3CVw(c)FF%PfPw; z;^`JVKOy;J#MA9)epK?$5Kp(V`CiFCMLgZY=DWa~N4Ix0pdUxen}}?C&s&MEoeit5 z-H@HDUN-?VwRmX|Mr@93mkk`JE+Lc4OFjd zS+uA8O>F=Bdbmv8y~mu~{tPsQnf`^h9Ig-Gbu05Y!pcPPbGH1u&@1~q+Rn94_VWiJ z)bZ>w)4yrB+O3D=F!Zxst7%l)%=8&EeGajF0bs*x4Og>W_2%T=WoG)Xyj#sfZ+qqD zWUDBnp)=E&ebfr#G}CWp%xl@~!p~IP4R5115UyVLG-W|d>xOQJPxI)Ockn1ZiX!ox;dWmA^rUh`&>h!ULk%1lf;BMsalkSWBbk-4D232ecJdEHnlY~EmaqO>g&ODw|VHQ*96^t)K=iI`UwmW%AF8D1abb+A>*JJCNP8| z(=DAZ^8LOzzH=9RAKkK7$5El+B~&$}dgIdCObV_VUd3^)j@|EVB>5Y3K2DXd zKm=$`9&>e>ebi|BH2mXatAzL%>FSo{Rc1o=1;^jqzv4Xl8{g<4uI?XQ!L=Vjy$iF< zC#1787+-HCj)|k&&*H+JIS)HCUH(Tj+UX1X&gAY%xw{axP%5Pilrm7tKq&*I43siZ z%0MXt{}UOYi7aE#b)5LHY7s>;e)=C@{ zIUiQD)!5Y`f>UI2#w>X-=JgLrx0b3~`w!agU{XPYjwh$#FnQNQadnMz+g7{dHPZ}T z+^gHD?HzB_CP1-q=b*(ge8_sIbCAtkJC<}XQ)x#R)n#l-z%DGA3h_t^VMs|JNvcuR zb!jfEm=d%x7m2dimAZ|F>v$Y}QLYv(fNhcn~wCSP*S(ekH$xk7QsIg4NB&}Ts zGnHOay8fts^IF~Sj2}$6=pIH=2iWbXz~l%SiGd*cHe=h(;#~2 z;4^GvagNroy~2%26QwVQX?R@fzfPD-cLP48_87vetUh{nk5fL;hE-! z<}PNl6QEu1WV6qM&Vc?M=q1pLpvJGV*`@eWb_w($(3wlwY%l1<=tEXHDzm-f4pKEGJ3yUi@&{S`y}a9)wETuex$ncV8w)Z z__hb{y0`WYg6r_tg1-}QXR{QqXsoI^9D?fPl9nptiDgx*TdHart18;i5o&7`e?Nz= zG=}K&RTUCH0{nvjejl(C_`3!Dd>K^vZq&X*hv6Kp!Ff~VXL9xCN?|~i)=m5=>`sFU zv#Mq?6xzA0s^XDK3%`w(GVTu`+KZgMNILK|8G5wJ2*s93pDpB*oVQdO5Z56m7oq#5 zobFfP$gDKwugX;?bWG&_YoyasRUevwu%)sMU6kz&Lw*YKr?ij5p&hC(k!$xW%0GEmAuDFdYplrm7tKq&+N>lxtneZlp7cs!A%URRip=*n`A zOwLFMfr$cHzMl-9>gt>ih6cubW1SG62d8Im_-YyW?Kc)fZsqWkr@w`uyArURND(DbmT zf2ir7X!??-XEhD_=R5`RApf`#L=-<4DU5$qG-fEx8G5Nf;qm5mrYS?hxfd)xzIgrU z5iRHSq57~Dye{+yTF&b}**~xMd_~K7Jtxa~9p^>%uh&=R{NEvD;q{oj{*vXq?vmG8 z^14d)&+939{UooK2MqA6{=5# zbl4Bm|Ednhy5>4DbF9zln2Tka#$~CvoDIt9`W(X71@0Hit28cjKN(-IalQ{Q{%KUo za5XfV6vD?rv&2^jzJ9a*@1jEOP6YfPkoq-(?+2{^2dET^|2x_bAOBHp@aLcuPo0j3 z#(>JJ_@j3Dc-NKkoW>*IsSqiYw-KpdE%t7ZYI#3} z^uGxGkXSDGm}Y|~rJtJwujgm{s}jFi1n&#q1dfkLx&241|AN$Cl{!3D`@NoRkl)+$W-HWpYu`kG z{`$W-9Uw;JuArzhptMFDZzvI^T(<3Q+|g>a?rfp0Qt(JkP?Y4NF9Z?DqVM@)^+z^ zwFb9B1-3W^c7&;oRrwpT0(+~l!OOStDzJr$HiPBu?`r(e#;rozyMnvKXkXe#*n1Yq z+lCcRj*obDp9m*CSJ5G^#S@+r9*n2LeY8z%Q!FYZH)JP=L^wJghoz#Pt0V^;SMCv$ zYMQYibDd$E40LUD*b`wnaA6M{%S2cn6=63KwLM#eogsay3`JqfoU%~ojI!W5Sy2;k zAj7doV*Q9G0mt|W%hr_VzGPB_aURP(bBjF-+a5OF^`&q$=dKa|jS9Ut(z_QfKe7b( zmDXyU2|wpDPZL8|@ovTTy}H7b?fLngpC{OUD=PFZ$@cud$&}_@s8G99WqW?_i~u9w zY|rnnO!<8kDza#I%nB1VdUs@deqUr7jGy(G?twkMi!#seqf8CmQZRlEizq52V|#x8 zWyz~#&sj|Qy9w(D<9|lmbN+>)H8qWJ;x!y> z{}^!UKie1E7p~3aSaY4|3H +#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/tapdown/lib/python3.11/site-packages/greenlet/tests/_test_extension_cpp.cpython-311-x86_64-linux-gnu.so b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_cpp_exception.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_cpp_exception.py new file mode 100644 index 0000000..fa4dc2e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_initialstub_already_started.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_initialstub_already_started.py new file mode 100644 index 0000000..c1a44ef --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_slp_switch.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_slp_switch.py new file mode 100644 index 0000000..0990526 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets.py new file mode 100644 index 0000000..e151b19 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets2.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_switch_three_greenlets2.py new file mode 100644 index 0000000..1f6b66b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_switch_two_greenlets.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/fail_switch_two_greenlets.py new file mode 100644 index 0000000..3e52345 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/leakcheck.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/leakcheck.py new file mode 100644 index 0000000..7046e41 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_contextvars.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_contextvars.py new file mode 100644 index 0000000..b0d1ccf --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_cpp.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_cpp.py new file mode 100644 index 0000000..2d0cc9c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_extension_interface.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_extension_interface.py new file mode 100644 index 0000000..34b6656 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_gc.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_gc.py new file mode 100644 index 0000000..994addb --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_generator.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_generator.py new file mode 100644 index 0000000..ca4a644 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_generator_nested.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_generator_nested.py new file mode 100644 index 0000000..8d752a6 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_greenlet.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_greenlet.py new file mode 100644 index 0000000..1fa4bd1 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_greenlet_trash.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_greenlet_trash.py new file mode 100644 index 0000000..c1fc137 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_leaks.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_leaks.py new file mode 100644 index 0000000..e09da7d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_stack_saved.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_stack_saved.py new file mode 100644 index 0000000..b362bf9 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_throw.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_throw.py new file mode 100644 index 0000000..f4f9a14 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_tracing.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_tracing.py new file mode 100644 index 0000000..235fbcd --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_version.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_version.py new file mode 100644 index 0000000..96c17cf --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/greenlet/tests/test_weakref.py b/tapdown/lib/python3.11/site-packages/greenlet/tests/test_weakref.py new file mode 100644 index 0000000..05a38a7 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/LICENSE b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/LICENSE new file mode 100644 index 0000000..0dca6e1 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/METADATA new file mode 100644 index 0000000..550aef2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/RECORD new file mode 100644 index 0000000..b2f4d33 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/RECORD @@ -0,0 +1,77 @@ +../../../bin/gunicorn,sha256=dtIoab4QMjuTIk_-1RzY4RobketAf7HKZUXhqUb5Mk8,235 +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/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/REQUESTED b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/WHEEL new file mode 100644 index 0000000..1a9c535 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/entry_points.txt b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/entry_points.txt new file mode 100644 index 0000000..fd14749 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/top_level.txt b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/top_level.txt new file mode 100644 index 0000000..8f22dcc --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/gunicorn-23.0.0.dist-info/top_level.txt @@ -0,0 +1 @@ +gunicorn diff --git a/tapdown/lib/python3.11/site-packages/gunicorn/__init__.py b/tapdown/lib/python3.11/site-packages/gunicorn/__init__.py new file mode 100644 index 0000000..cdcd135 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/__main__.py b/tapdown/lib/python3.11/site-packages/gunicorn/__main__.py new file mode 100644 index 0000000..ceb44d0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/app/__init__.py b/tapdown/lib/python3.11/site-packages/gunicorn/app/__init__.py new file mode 100644 index 0000000..530e35c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/app/base.py b/tapdown/lib/python3.11/site-packages/gunicorn/app/base.py new file mode 100644 index 0000000..9bf7a4f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/app/pasterapp.py b/tapdown/lib/python3.11/site-packages/gunicorn/app/pasterapp.py new file mode 100644 index 0000000..b1738f2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/app/wsgiapp.py b/tapdown/lib/python3.11/site-packages/gunicorn/app/wsgiapp.py new file mode 100644 index 0000000..1b0ba96 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/arbiter.py b/tapdown/lib/python3.11/site-packages/gunicorn/arbiter.py new file mode 100644 index 0000000..1eaf453 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/config.py b/tapdown/lib/python3.11/site-packages/gunicorn/config.py new file mode 100644 index 0000000..402a26b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/debug.py b/tapdown/lib/python3.11/site-packages/gunicorn/debug.py new file mode 100644 index 0000000..5fae0b4 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/errors.py b/tapdown/lib/python3.11/site-packages/gunicorn/errors.py new file mode 100644 index 0000000..1128380 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/glogging.py b/tapdown/lib/python3.11/site-packages/gunicorn/glogging.py new file mode 100644 index 0000000..e34fcd5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/http/__init__.py b/tapdown/lib/python3.11/site-packages/gunicorn/http/__init__.py new file mode 100644 index 0000000..11473bb --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/http/body.py b/tapdown/lib/python3.11/site-packages/gunicorn/http/body.py new file mode 100644 index 0000000..d7ee29e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/http/errors.py b/tapdown/lib/python3.11/site-packages/gunicorn/http/errors.py new file mode 100644 index 0000000..bcb9700 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/http/message.py b/tapdown/lib/python3.11/site-packages/gunicorn/http/message.py new file mode 100644 index 0000000..59ce0bf --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/http/parser.py b/tapdown/lib/python3.11/site-packages/gunicorn/http/parser.py new file mode 100644 index 0000000..88da17a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/http/unreader.py b/tapdown/lib/python3.11/site-packages/gunicorn/http/unreader.py new file mode 100644 index 0000000..9aadfbc --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/http/wsgi.py b/tapdown/lib/python3.11/site-packages/gunicorn/http/wsgi.py new file mode 100644 index 0000000..419ac50 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/instrument/__init__.py b/tapdown/lib/python3.11/site-packages/gunicorn/instrument/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/gunicorn/instrument/statsd.py b/tapdown/lib/python3.11/site-packages/gunicorn/instrument/statsd.py new file mode 100644 index 0000000..7bc4e6f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/pidfile.py b/tapdown/lib/python3.11/site-packages/gunicorn/pidfile.py new file mode 100644 index 0000000..b171f7d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/reloader.py b/tapdown/lib/python3.11/site-packages/gunicorn/reloader.py new file mode 100644 index 0000000..1c67f2a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/sock.py b/tapdown/lib/python3.11/site-packages/gunicorn/sock.py new file mode 100644 index 0000000..eb2b6fa --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/systemd.py b/tapdown/lib/python3.11/site-packages/gunicorn/systemd.py new file mode 100644 index 0000000..9b18550 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/gunicorn/util.py b/tapdown/lib/python3.11/site-packages/gunicorn/util.py new file mode 100644 index 0000000..ecd8174 --- /dev/null +++ b/tapdown/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 + + +

+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 literal 0 HcmV?d00001 diff --git a/tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/rebuild.py b/tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/rebuild.py new file mode 100644 index 0000000..684c658 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dateutil/zoneinfo/rebuild.py @@ -0,0 +1,75 @@ +import logging +import os +import tempfile +import shutil +import json +from subprocess import check_call, check_output +from tarfile import TarFile + +from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME + + +def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None): + """Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar* + + filename is the timezone tarball from ``ftp.iana.org/tz``. + + """ + tmpdir = tempfile.mkdtemp() + zonedir = os.path.join(tmpdir, "zoneinfo") + moduledir = os.path.dirname(__file__) + try: + with TarFile.open(filename) as tf: + for name in zonegroups: + tf.extract(name, tmpdir) + filepaths = [os.path.join(tmpdir, n) for n in zonegroups] + + _run_zic(zonedir, filepaths) + + # write metadata file + with open(os.path.join(zonedir, METADATA_FN), 'w') as f: + json.dump(metadata, f, indent=4, sort_keys=True) + target = os.path.join(moduledir, ZONEFILENAME) + with TarFile.open(target, "w:%s" % format) as tf: + for entry in os.listdir(zonedir): + entrypath = os.path.join(zonedir, entry) + tf.add(entrypath, entry) + finally: + shutil.rmtree(tmpdir) + + +def _run_zic(zonedir, filepaths): + """Calls the ``zic`` compiler in a compatible way to get a "fat" binary. + + Recent versions of ``zic`` default to ``-b slim``, while older versions + don't even have the ``-b`` option (but default to "fat" binaries). The + current version of dateutil does not support Version 2+ TZif files, which + causes problems when used in conjunction with "slim" binaries, so this + function is used to ensure that we always get a "fat" binary. + """ + + try: + help_text = check_output(["zic", "--help"]) + except OSError as e: + _print_on_nosuchfile(e) + raise + + if b"-b " in help_text: + bloat_args = ["-b", "fat"] + else: + bloat_args = [] + + check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths) + + +def _print_on_nosuchfile(e): + """Print helpful troubleshooting message + + e is an exception raised by subprocess.check_call() + + """ + if e.errno == 2: + logging.error( + "Could not find zic. Perhaps you need to install " + "libc-bin or some other package that provides it, " + "or it's not in your PATH?") diff --git a/tapdown/lib/python3.11/site-packages/distutils-precedence.pth b/tapdown/lib/python3.11/site-packages/distutils-precedence.pth new file mode 100644 index 0000000..7f009fe --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/__init__.py b/tapdown/lib/python3.11/site-packages/dns/__init__.py new file mode 100644 index 0000000..d30fd74 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/_asyncbackend.py b/tapdown/lib/python3.11/site-packages/dns/_asyncbackend.py new file mode 100644 index 0000000..23455db --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/_asyncio_backend.py b/tapdown/lib/python3.11/site-packages/dns/_asyncio_backend.py new file mode 100644 index 0000000..303908c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/_ddr.py b/tapdown/lib/python3.11/site-packages/dns/_ddr.py new file mode 100644 index 0000000..bf5c11e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/_features.py b/tapdown/lib/python3.11/site-packages/dns/_features.py new file mode 100644 index 0000000..65a9a2a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/_immutable_ctx.py b/tapdown/lib/python3.11/site-packages/dns/_immutable_ctx.py new file mode 100644 index 0000000..b3d72de --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/_no_ssl.py b/tapdown/lib/python3.11/site-packages/dns/_no_ssl.py new file mode 100644 index 0000000..edb452d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/_tls_util.py b/tapdown/lib/python3.11/site-packages/dns/_tls_util.py new file mode 100644 index 0000000..10ddf72 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/_trio_backend.py b/tapdown/lib/python3.11/site-packages/dns/_trio_backend.py new file mode 100644 index 0000000..bde7e8b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/asyncbackend.py b/tapdown/lib/python3.11/site-packages/dns/asyncbackend.py new file mode 100644 index 0000000..0ec58b0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/asyncquery.py b/tapdown/lib/python3.11/site-packages/dns/asyncquery.py new file mode 100644 index 0000000..bb77045 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/asyncresolver.py b/tapdown/lib/python3.11/site-packages/dns/asyncresolver.py new file mode 100644 index 0000000..6f8c69f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/btree.py b/tapdown/lib/python3.11/site-packages/dns/btree.py new file mode 100644 index 0000000..12da9f5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/btreezone.py b/tapdown/lib/python3.11/site-packages/dns/btreezone.py new file mode 100644 index 0000000..27b5bb6 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/dnssec.py b/tapdown/lib/python3.11/site-packages/dns/dnssec.py new file mode 100644 index 0000000..0b2aa70 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/__init__.py b/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/__init__.py new file mode 100644 index 0000000..0810b19 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/base.py b/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/base.py new file mode 100644 index 0000000..0334fe6 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/cryptography.py b/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/cryptography.py new file mode 100644 index 0000000..a5dde6a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/dsa.py b/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/dsa.py new file mode 100644 index 0000000..a4eb987 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/ecdsa.py b/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/ecdsa.py new file mode 100644 index 0000000..e3f3f06 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/eddsa.py b/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/eddsa.py new file mode 100644 index 0000000..1cbb407 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/rsa.py b/tapdown/lib/python3.11/site-packages/dns/dnssecalgs/rsa.py new file mode 100644 index 0000000..de9160b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/dnssectypes.py b/tapdown/lib/python3.11/site-packages/dns/dnssectypes.py new file mode 100644 index 0000000..02131e0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/e164.py b/tapdown/lib/python3.11/site-packages/dns/e164.py new file mode 100644 index 0000000..942d2c0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/edns.py b/tapdown/lib/python3.11/site-packages/dns/edns.py new file mode 100644 index 0000000..eb98548 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/entropy.py b/tapdown/lib/python3.11/site-packages/dns/entropy.py new file mode 100644 index 0000000..6430926 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/enum.py b/tapdown/lib/python3.11/site-packages/dns/enum.py new file mode 100644 index 0000000..822c995 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/exception.py b/tapdown/lib/python3.11/site-packages/dns/exception.py new file mode 100644 index 0000000..c3d42ff --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/flags.py b/tapdown/lib/python3.11/site-packages/dns/flags.py new file mode 100644 index 0000000..4c60be1 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/grange.py b/tapdown/lib/python3.11/site-packages/dns/grange.py new file mode 100644 index 0000000..8d366dc --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/immutable.py b/tapdown/lib/python3.11/site-packages/dns/immutable.py new file mode 100644 index 0000000..36b0362 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/inet.py b/tapdown/lib/python3.11/site-packages/dns/inet.py new file mode 100644 index 0000000..765203b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/ipv4.py b/tapdown/lib/python3.11/site-packages/dns/ipv4.py new file mode 100644 index 0000000..a7161bc --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/ipv6.py b/tapdown/lib/python3.11/site-packages/dns/ipv6.py new file mode 100644 index 0000000..eaa0f6c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/message.py b/tapdown/lib/python3.11/site-packages/dns/message.py new file mode 100644 index 0000000..bbfccfc --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/name.py b/tapdown/lib/python3.11/site-packages/dns/name.py new file mode 100644 index 0000000..45c8f45 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/namedict.py b/tapdown/lib/python3.11/site-packages/dns/namedict.py new file mode 100644 index 0000000..ca8b197 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/nameserver.py b/tapdown/lib/python3.11/site-packages/dns/nameserver.py new file mode 100644 index 0000000..c9307d3 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/node.py b/tapdown/lib/python3.11/site-packages/dns/node.py new file mode 100644 index 0000000..b2cbf1b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/opcode.py b/tapdown/lib/python3.11/site-packages/dns/opcode.py new file mode 100644 index 0000000..3fa610d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/py.typed b/tapdown/lib/python3.11/site-packages/dns/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/dns/query.py b/tapdown/lib/python3.11/site-packages/dns/query.py new file mode 100644 index 0000000..17b1862 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/quic/__init__.py b/tapdown/lib/python3.11/site-packages/dns/quic/__init__.py new file mode 100644 index 0000000..7c2a699 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/quic/_asyncio.py b/tapdown/lib/python3.11/site-packages/dns/quic/_asyncio.py new file mode 100644 index 0000000..0a177b6 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/quic/_common.py b/tapdown/lib/python3.11/site-packages/dns/quic/_common.py new file mode 100644 index 0000000..ba9d245 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/quic/_sync.py b/tapdown/lib/python3.11/site-packages/dns/quic/_sync.py new file mode 100644 index 0000000..18f9d05 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/quic/_trio.py b/tapdown/lib/python3.11/site-packages/dns/quic/_trio.py new file mode 100644 index 0000000..046e6aa --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rcode.py b/tapdown/lib/python3.11/site-packages/dns/rcode.py new file mode 100644 index 0000000..7bb8467 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdata.py b/tapdown/lib/python3.11/site-packages/dns/rdata.py new file mode 100644 index 0000000..c4522e6 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdataclass.py b/tapdown/lib/python3.11/site-packages/dns/rdataclass.py new file mode 100644 index 0000000..89b85a7 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdataset.py b/tapdown/lib/python3.11/site-packages/dns/rdataset.py new file mode 100644 index 0000000..1edf67d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdatatype.py b/tapdown/lib/python3.11/site-packages/dns/rdatatype.py new file mode 100644 index 0000000..211d810 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/AFSDB.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/AFSDB.py new file mode 100644 index 0000000..06a3b97 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/AMTRELAY.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/AMTRELAY.py new file mode 100644 index 0000000..dc9fa87 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/AVC.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/AVC.py new file mode 100644 index 0000000..a27ae2d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CAA.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CAA.py new file mode 100644 index 0000000..8c62e62 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CDNSKEY.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CDNSKEY.py new file mode 100644 index 0000000..b613409 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CDS.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CDS.py new file mode 100644 index 0000000..8312b97 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CERT.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CERT.py new file mode 100644 index 0000000..4d5e5bd --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CNAME.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CNAME.py new file mode 100644 index 0000000..665e407 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CSYNC.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/CSYNC.py new file mode 100644 index 0000000..103486d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DLV.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DLV.py new file mode 100644 index 0000000..6c134f1 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DNAME.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DNAME.py new file mode 100644 index 0000000..bbf9186 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DNSKEY.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DNSKEY.py new file mode 100644 index 0000000..6d961a9 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DS.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DS.py new file mode 100644 index 0000000..58b3108 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DSYNC.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/DSYNC.py new file mode 100644 index 0000000..e8d1394 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI48.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI48.py new file mode 100644 index 0000000..c843be5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI64.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/EUI64.py new file mode 100644 index 0000000..f6d7e25 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/GPOS.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/GPOS.py new file mode 100644 index 0000000..d79f4a0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/HINFO.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/HINFO.py new file mode 100644 index 0000000..06ad348 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/HIP.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/HIP.py new file mode 100644 index 0000000..dc7948a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/ISDN.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/ISDN.py new file mode 100644 index 0000000..6428a0a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/L32.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/L32.py new file mode 100644 index 0000000..f51e5c7 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/L64.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/L64.py new file mode 100644 index 0000000..a47da19 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/LOC.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/LOC.py new file mode 100644 index 0000000..6c7fe5e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/LP.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/LP.py new file mode 100644 index 0000000..379c862 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/MX.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/MX.py new file mode 100644 index 0000000..0c300c5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NID.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NID.py new file mode 100644 index 0000000..fa0dad5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NINFO.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NINFO.py new file mode 100644 index 0000000..b177bdd --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NS.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NS.py new file mode 100644 index 0000000..c3f34ce --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC.py new file mode 100644 index 0000000..3c78b72 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3.py new file mode 100644 index 0000000..6899418 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/NSEC3PARAM.py new file mode 100644 index 0000000..e867872 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/OPENPGPKEY.py new file mode 100644 index 0000000..ac1841c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/OPT.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/OPT.py new file mode 100644 index 0000000..d343dfa --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/PTR.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/PTR.py new file mode 100644 index 0000000..98c3616 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RESINFO.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RESINFO.py new file mode 100644 index 0000000..76c8ea2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RP.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RP.py new file mode 100644 index 0000000..a66cfc5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RRSIG.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RRSIG.py new file mode 100644 index 0000000..5556cba --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RT.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/RT.py new file mode 100644 index 0000000..5a4d45c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SMIMEA.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SMIMEA.py new file mode 100644 index 0000000..55d87bf --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SOA.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SOA.py new file mode 100644 index 0000000..3c7cd8c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SPF.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SPF.py new file mode 100644 index 0000000..1df3b70 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SSHFP.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/SSHFP.py new file mode 100644 index 0000000..3f08f3a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TKEY.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TKEY.py new file mode 100644 index 0000000..f9189b1 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TLSA.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TLSA.py new file mode 100644 index 0000000..4dffc55 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TSIG.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TSIG.py new file mode 100644 index 0000000..7942382 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TXT.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/TXT.py new file mode 100644 index 0000000..6d4dae2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/URI.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/URI.py new file mode 100644 index 0000000..021391d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/WALLET.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/WALLET.py new file mode 100644 index 0000000..ff46476 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/X25.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/X25.py new file mode 100644 index 0000000..2436ddb --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/ZONEMD.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/ZONEMD.py new file mode 100644 index 0000000..acef4f2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/__init__.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/ANY/__init__.py new file mode 100644 index 0000000..cc39f86 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/CH/A.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/CH/A.py new file mode 100644 index 0000000..e3e0752 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/CH/__init__.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/CH/__init__.py new file mode 100644 index 0000000..0760c26 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/A.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/A.py new file mode 100644 index 0000000..e09d611 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/AAAA.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/AAAA.py new file mode 100644 index 0000000..0cd139e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/APL.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/APL.py new file mode 100644 index 0000000..c4ce6e4 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/DHCID.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/DHCID.py new file mode 100644 index 0000000..8de8cdf --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/HTTPS.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/HTTPS.py new file mode 100644 index 0000000..15464cb --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/IPSECKEY.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/IPSECKEY.py new file mode 100644 index 0000000..aef93ae --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/KX.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/KX.py new file mode 100644 index 0000000..6073df4 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/NAPTR.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/NAPTR.py new file mode 100644 index 0000000..98bbf4a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP.py new file mode 100644 index 0000000..d55edb7 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP_PTR.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/NSAP_PTR.py new file mode 100644 index 0000000..ce1c663 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/PX.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/PX.py new file mode 100644 index 0000000..20143bf --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/SRV.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/SRV.py new file mode 100644 index 0000000..044c10e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/SVCB.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/SVCB.py new file mode 100644 index 0000000..ff3e932 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/WKS.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/WKS.py new file mode 100644 index 0000000..cc6c373 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/__init__.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/IN/__init__.py new file mode 100644 index 0000000..dcec4dd --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/__init__.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/__init__.py new file mode 100644 index 0000000..3997f84 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/dnskeybase.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/dnskeybase.py new file mode 100644 index 0000000..fb49f92 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/dsbase.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/dsbase.py new file mode 100644 index 0000000..8e05c2a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/euibase.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/euibase.py new file mode 100644 index 0000000..4eb82eb --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/mxbase.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/mxbase.py new file mode 100644 index 0000000..5d33e61 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/nsbase.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/nsbase.py new file mode 100644 index 0000000..904224f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/svcbbase.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/svcbbase.py new file mode 100644 index 0000000..7338b66 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/tlsabase.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/tlsabase.py new file mode 100644 index 0000000..ddc196f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/txtbase.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/txtbase.py new file mode 100644 index 0000000..5e5b24f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rdtypes/util.py b/tapdown/lib/python3.11/site-packages/dns/rdtypes/util.py new file mode 100644 index 0000000..c17b154 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/renderer.py b/tapdown/lib/python3.11/site-packages/dns/renderer.py new file mode 100644 index 0000000..cc912b2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/resolver.py b/tapdown/lib/python3.11/site-packages/dns/resolver.py new file mode 100644 index 0000000..923bb4b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/reversename.py b/tapdown/lib/python3.11/site-packages/dns/reversename.py new file mode 100644 index 0000000..60a4e83 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/rrset.py b/tapdown/lib/python3.11/site-packages/dns/rrset.py new file mode 100644 index 0000000..271ddbe --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/serial.py b/tapdown/lib/python3.11/site-packages/dns/serial.py new file mode 100644 index 0000000..3417299 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/set.py b/tapdown/lib/python3.11/site-packages/dns/set.py new file mode 100644 index 0000000..ae8f0dd --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/tokenizer.py b/tapdown/lib/python3.11/site-packages/dns/tokenizer.py new file mode 100644 index 0000000..86ae3e2 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/transaction.py b/tapdown/lib/python3.11/site-packages/dns/transaction.py new file mode 100644 index 0000000..9ecd737 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/tsig.py b/tapdown/lib/python3.11/site-packages/dns/tsig.py new file mode 100644 index 0000000..333f9aa --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/ttl.py b/tapdown/lib/python3.11/site-packages/dns/ttl.py new file mode 100644 index 0000000..16289cd --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/update.py b/tapdown/lib/python3.11/site-packages/dns/update.py new file mode 100644 index 0000000..0e4aee4 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/version.py b/tapdown/lib/python3.11/site-packages/dns/version.py new file mode 100644 index 0000000..e11dd29 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/versioned.py b/tapdown/lib/python3.11/site-packages/dns/versioned.py new file mode 100644 index 0000000..3644711 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/win32util.py b/tapdown/lib/python3.11/site-packages/dns/win32util.py new file mode 100644 index 0000000..2d77b4c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/wire.py b/tapdown/lib/python3.11/site-packages/dns/wire.py new file mode 100644 index 0000000..cd027fa --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/xfr.py b/tapdown/lib/python3.11/site-packages/dns/xfr.py new file mode 100644 index 0000000..219fdc8 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/zone.py b/tapdown/lib/python3.11/site-packages/dns/zone.py new file mode 100644 index 0000000..f916ffe --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/zonefile.py b/tapdown/lib/python3.11/site-packages/dns/zonefile.py new file mode 100644 index 0000000..7a81454 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dns/zonetypes.py b/tapdown/lib/python3.11/site-packages/dns/zonetypes.py new file mode 100644 index 0000000..195ee2e --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/METADATA new file mode 100644 index 0000000..eaaf09b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/RECORD new file mode 100644 index 0000000..397075c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/WHEEL new file mode 100644 index 0000000..12228d4 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/licenses/LICENSE b/tapdown/lib/python3.11/site-packages/dnspython-2.8.0.dist-info/licenses/LICENSE new file mode 100644 index 0000000..390a726 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/engineio/__init__.py b/tapdown/lib/python3.11/site-packages/engineio/__init__.py new file mode 100644 index 0000000..4919efd --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/__init__.py @@ -0,0 +1,13 @@ +from .client import Client +from .middleware import WSGIApp, Middleware +from .server import Server +from .async_server import AsyncServer +from .async_client import AsyncClient +from .async_drivers.asgi import ASGIApp +try: + from .async_drivers.tornado import get_tornado_handler +except ImportError: # pragma: no cover + get_tornado_handler = None + +__all__ = ['Server', 'WSGIApp', 'Middleware', 'Client', + 'AsyncServer', 'ASGIApp', 'get_tornado_handler', 'AsyncClient'] diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_client.py b/tapdown/lib/python3.11/site-packages/engineio/async_client.py new file mode 100644 index 0000000..43f3a56 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_client.py @@ -0,0 +1,689 @@ +import asyncio +import signal +import ssl +import threading + +try: + import aiohttp +except ImportError: # pragma: no cover + aiohttp = None + +from . import base_client +from . import exceptions +from . import packet +from . import payload + +async_signal_handler_set = False + +# this set is used to keep references to background tasks to prevent them from +# being garbage collected mid-execution. Solution taken from +# https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task +task_reference_holder = set() + + +def async_signal_handler(): + """SIGINT handler. + + Disconnect all active async clients. + """ + async def _handler(): # pragma: no cover + for c in base_client.connected_clients[:]: + if c.is_asyncio_based(): + await c.disconnect() + + # cancel all running tasks + tasks = [task for task in asyncio.all_tasks() if task is not + asyncio.current_task()] + for task in tasks: + task.cancel() + await asyncio.gather(*tasks, return_exceptions=True) + asyncio.get_running_loop().stop() + + asyncio.ensure_future(_handler()) + + +class AsyncClient(base_client.BaseClient): + """An Engine.IO client for asyncio. + + This class implements a fully compliant Engine.IO web client with support + for websocket and long-polling transports, compatible with the asyncio + framework on Python 3.5 or newer. + + :param logger: To enable logging set to ``True`` or pass a logger object to + use. To disable logging set to ``False``. The default is + ``False``. Note that fatal errors are logged even when + ``logger`` is ``False``. + :param json: An alternative json module to use for encoding and decoding + packets. Custom json modules must have ``dumps`` and ``loads`` + functions that are compatible with the standard library + versions. + :param request_timeout: A timeout in seconds for requests. The default is + 5 seconds. + :param http_session: an initialized ``aiohttp.ClientSession`` object to be + used when sending requests to the server. Use it if + you need to add special client options such as proxy + servers, SSL certificates, custom CA bundle, etc. + :param ssl_verify: ``True`` to verify SSL certificates, or ``False`` to + skip SSL certificate verification, allowing + connections to servers with self signed certificates. + The default is ``True``. + :param handle_sigint: Set to ``True`` to automatically handle disconnection + when the process is interrupted, or to ``False`` to + leave interrupt handling to the calling application. + Interrupt handling can only be enabled when the + client instance is created in the main thread. + :param websocket_extra_options: Dictionary containing additional keyword + arguments passed to + ``aiohttp.ws_connect()``. + :param timestamp_requests: If ``True`` a timestamp is added to the query + string of Socket.IO requests as a cache-busting + measure. Set to ``False`` to disable. + """ + def is_asyncio_based(self): + return True + + async def connect(self, url, headers=None, transports=None, + engineio_path='engine.io'): + """Connect to an Engine.IO server. + + :param url: The URL of the Engine.IO server. It can include custom + query string parameters if required by the server. + :param headers: A dictionary with custom headers to send with the + connection request. + :param transports: The list of allowed transports. Valid transports + are ``'polling'`` and ``'websocket'``. If not + given, the polling transport is connected first, + then an upgrade to websocket is attempted. + :param engineio_path: The endpoint where the Engine.IO server is + installed. The default value is appropriate for + most cases. + + Note: this method is a coroutine. + + Example usage:: + + eio = engineio.Client() + await eio.connect('http://localhost:5000') + """ + global async_signal_handler_set + if self.handle_sigint and not async_signal_handler_set and \ + threading.current_thread() == threading.main_thread(): + try: + asyncio.get_running_loop().add_signal_handler( + signal.SIGINT, async_signal_handler) + except NotImplementedError: # pragma: no cover + self.logger.warning('Signal handler is unsupported') + async_signal_handler_set = True + + if self.state != 'disconnected': + raise ValueError('Client is not in a disconnected state') + valid_transports = ['polling', 'websocket'] + if transports is not None: + if isinstance(transports, str): + transports = [transports] + transports = [transport for transport in transports + if transport in valid_transports] + if not transports: + raise ValueError('No valid transports provided') + self.transports = transports or valid_transports + return await getattr(self, '_connect_' + self.transports[0])( + url, headers or {}, engineio_path) + + async def wait(self): + """Wait until the connection with the server ends. + + Client applications can use this function to block the main thread + during the life of the connection. + + Note: this method is a coroutine. + """ + if self.read_loop_task: + await self.read_loop_task + + async def send(self, data): + """Send a message to the server. + + :param data: The data to send to the server. Data can be of type + ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` + or ``dict``, the data will be serialized as JSON. + + Note: this method is a coroutine. + """ + await self._send_packet(packet.Packet(packet.MESSAGE, data=data)) + + async def disconnect(self, abort=False, reason=None): + """Disconnect from the server. + + :param abort: If set to ``True``, do not wait for background tasks + associated with the connection to end. + + Note: this method is a coroutine. + """ + if self.state == 'connected': + await self._send_packet(packet.Packet(packet.CLOSE)) + await self.queue.put(None) + self.state = 'disconnecting' + await self._trigger_event('disconnect', + reason or self.reason.CLIENT_DISCONNECT, + run_async=False) + if self.current_transport == 'websocket': + await self.ws.close() + if not abort: + await self.read_loop_task + self.state = 'disconnected' + try: + base_client.connected_clients.remove(self) + except ValueError: # pragma: no cover + pass + await self._reset() + + def start_background_task(self, target, *args, **kwargs): + """Start a background task. + + This is a utility function that applications can use to start a + background task. + + :param target: the target function to execute. + :param args: arguments to pass to the function. + :param kwargs: keyword arguments to pass to the function. + + The return value is a ``asyncio.Task`` object. + """ + return asyncio.ensure_future(target(*args, **kwargs)) + + async def sleep(self, seconds=0): + """Sleep for the requested amount of time. + + Note: this method is a coroutine. + """ + return await asyncio.sleep(seconds) + + def create_queue(self, *args, **kwargs): + """Create a queue object.""" + return asyncio.Queue(*args, **kwargs) + + def get_queue_empty_exception(self): + """Return the queue empty exception raised by queues created by the + ``create_queue()`` method. + """ + return asyncio.QueueEmpty + + def create_event(self): + """Create an event object.""" + return asyncio.Event() + + async def _reset(self): + super()._reset() + while True: # pragma: no cover + try: + self.queue.get_nowait() + self.queue.task_done() + except self.queue_empty: + break + if not self.external_http: # pragma: no cover + if self.http and not self.http.closed: + await self.http.close() + + def __del__(self): # pragma: no cover + # try to close the aiohttp session if it is still open + if self.http and not self.http.closed: + try: + loop = asyncio.get_event_loop() + if loop.is_running(): + loop.ensure_future(self.http.close()) + else: + loop.run_until_complete(self.http.close()) + except: + pass + + async def _connect_polling(self, url, headers, engineio_path): + """Establish a long-polling connection to the Engine.IO server.""" + if aiohttp is None: # pragma: no cover + self.logger.error('aiohttp not installed -- cannot make HTTP ' + 'requests!') + return + self.base_url = self._get_engineio_url(url, engineio_path, 'polling') + self.logger.info('Attempting polling connection to ' + self.base_url) + r = await self._send_request( + 'GET', self.base_url + self._get_url_timestamp(), headers=headers, + timeout=self.request_timeout) + if r is None or isinstance(r, str): + await self._reset() + raise exceptions.ConnectionError( + r or 'Connection refused by the server') + if r.status < 200 or r.status >= 300: + await self._reset() + try: + arg = await r.json() + except aiohttp.ClientError: + arg = None + raise exceptions.ConnectionError( + 'Unexpected status code {} in server response'.format( + r.status), arg) + try: + p = payload.Payload(encoded_payload=(await r.read()).decode( + 'utf-8')) + except ValueError: + raise exceptions.ConnectionError( + 'Unexpected response from server') from None + open_packet = p.packets[0] + if open_packet.packet_type != packet.OPEN: + raise exceptions.ConnectionError( + 'OPEN packet not returned by server') + self.logger.info( + 'Polling connection accepted with ' + str(open_packet.data)) + self.sid = open_packet.data['sid'] + self.upgrades = open_packet.data['upgrades'] + self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 + self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 + self.current_transport = 'polling' + self.base_url += '&sid=' + self.sid + + self.state = 'connected' + base_client.connected_clients.append(self) + await self._trigger_event('connect', run_async=False) + + for pkt in p.packets[1:]: + await self._receive_packet(pkt) + + if 'websocket' in self.upgrades and 'websocket' in self.transports: + # attempt to upgrade to websocket + if await self._connect_websocket(url, headers, engineio_path): + # upgrade to websocket succeeded, we're done here + return + + self.write_loop_task = self.start_background_task(self._write_loop) + self.read_loop_task = self.start_background_task( + self._read_loop_polling) + + async def _connect_websocket(self, url, headers, engineio_path): + """Establish or upgrade to a WebSocket connection with the server.""" + if aiohttp is None: # pragma: no cover + self.logger.error('aiohttp package not installed') + return False + websocket_url = self._get_engineio_url(url, engineio_path, + 'websocket') + if self.sid: + self.logger.info( + 'Attempting WebSocket upgrade to ' + websocket_url) + upgrade = True + websocket_url += '&sid=' + self.sid + else: + upgrade = False + self.base_url = websocket_url + self.logger.info( + 'Attempting WebSocket connection to ' + websocket_url) + + if self.http is None or self.http.closed: # pragma: no cover + self.http = aiohttp.ClientSession() + + # extract any new cookies passed in a header so that they can also be + # sent the the WebSocket route + cookies = {} + for header, value in headers.items(): + if header.lower() == 'cookie': + cookies = dict( + [cookie.split('=', 1) for cookie in value.split('; ')]) + del headers[header] + break + self.http.cookie_jar.update_cookies(cookies) + + extra_options = {'timeout': self.request_timeout} + if not self.ssl_verify: + ssl_context = ssl.create_default_context() + ssl_context.check_hostname = False + ssl_context.verify_mode = ssl.CERT_NONE + extra_options['ssl'] = ssl_context + + # combine internally generated options with the ones supplied by the + # caller. The caller's options take precedence. + headers.update(self.websocket_extra_options.pop('headers', {})) + extra_options['headers'] = headers + extra_options.update(self.websocket_extra_options) + + try: + ws = await self.http.ws_connect( + websocket_url + self._get_url_timestamp(), **extra_options) + except (aiohttp.client_exceptions.WSServerHandshakeError, + aiohttp.client_exceptions.ServerConnectionError, + aiohttp.client_exceptions.ClientConnectionError): + if upgrade: + self.logger.warning( + 'WebSocket upgrade failed: connection error') + return False + else: + raise exceptions.ConnectionError('Connection error') + if upgrade: + p = packet.Packet(packet.PING, data='probe').encode() + try: + await ws.send_str(p) + except Exception as e: # pragma: no cover + self.logger.warning( + 'WebSocket upgrade failed: unexpected send exception: %s', + str(e)) + return False + try: + p = (await ws.receive()).data + except Exception as e: # pragma: no cover + self.logger.warning( + 'WebSocket upgrade failed: unexpected recv exception: %s', + str(e)) + return False + pkt = packet.Packet(encoded_packet=p) + if pkt.packet_type != packet.PONG or pkt.data != 'probe': + self.logger.warning( + 'WebSocket upgrade failed: no PONG packet') + return False + p = packet.Packet(packet.UPGRADE).encode() + try: + await ws.send_str(p) + except Exception as e: # pragma: no cover + self.logger.warning( + 'WebSocket upgrade failed: unexpected send exception: %s', + str(e)) + return False + self.current_transport = 'websocket' + self.logger.info('WebSocket upgrade was successful') + else: + try: + p = (await ws.receive()).data + except Exception as e: # pragma: no cover + raise exceptions.ConnectionError( + 'Unexpected recv exception: ' + str(e)) + open_packet = packet.Packet(encoded_packet=p) + if open_packet.packet_type != packet.OPEN: + raise exceptions.ConnectionError('no OPEN packet') + self.logger.info( + 'WebSocket connection accepted with ' + str(open_packet.data)) + self.sid = open_packet.data['sid'] + self.upgrades = open_packet.data['upgrades'] + self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 + self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 + self.current_transport = 'websocket' + + self.state = 'connected' + base_client.connected_clients.append(self) + await self._trigger_event('connect', run_async=False) + + self.ws = ws + self.write_loop_task = self.start_background_task(self._write_loop) + self.read_loop_task = self.start_background_task( + self._read_loop_websocket) + return True + + async def _receive_packet(self, pkt): + """Handle incoming packets from the server.""" + packet_name = packet.packet_names[pkt.packet_type] \ + if pkt.packet_type < len(packet.packet_names) else 'UNKNOWN' + self.logger.info( + 'Received packet %s data %s', packet_name, + pkt.data if not isinstance(pkt.data, bytes) else '') + if pkt.packet_type == packet.MESSAGE: + await self._trigger_event('message', pkt.data, run_async=True) + elif pkt.packet_type == packet.PING: + await self._send_packet(packet.Packet(packet.PONG, pkt.data)) + elif pkt.packet_type == packet.CLOSE: + await self.disconnect(abort=True, + reason=self.reason.SERVER_DISCONNECT) + elif pkt.packet_type == packet.NOOP: + pass + else: + self.logger.error('Received unexpected packet of type %s', + pkt.packet_type) + + async def _send_packet(self, pkt): + """Queue a packet to be sent to the server.""" + if self.state != 'connected': + return + await self.queue.put(pkt) + self.logger.info( + 'Sending packet %s data %s', + packet.packet_names[pkt.packet_type], + pkt.data if not isinstance(pkt.data, bytes) else '') + + async def _send_request( + self, method, url, headers=None, body=None, + timeout=None): # pragma: no cover + if self.http is None or self.http.closed: + self.http = aiohttp.ClientSession() + http_method = getattr(self.http, method.lower()) + + try: + if not self.ssl_verify: + return await http_method( + url, headers=headers, data=body, + timeout=aiohttp.ClientTimeout(total=timeout), ssl=False) + else: + return await http_method( + url, headers=headers, data=body, + timeout=aiohttp.ClientTimeout(total=timeout)) + + except (aiohttp.ClientError, asyncio.TimeoutError) as exc: + self.logger.info('HTTP %s request to %s failed with error %s.', + method, url, exc) + return str(exc) + + async def _trigger_event(self, event, *args, **kwargs): + """Invoke an event handler.""" + run_async = kwargs.pop('run_async', False) + ret = None + if event in self.handlers: + if asyncio.iscoroutinefunction(self.handlers[event]) is True: + if run_async: + task = self.start_background_task(self.handlers[event], + *args) + task_reference_holder.add(task) + task.add_done_callback(task_reference_holder.discard) + return task + else: + try: + try: + ret = await self.handlers[event](*args) + except TypeError: + if event == 'disconnect' and \ + len(args) == 1: # pragma: no branch + # legacy disconnect events do not have a reason + # argument + return await self.handlers[event]() + else: # pragma: no cover + raise + except asyncio.CancelledError: # pragma: no cover + pass + except: + self.logger.exception(event + ' async handler error') + if event == 'connect': + # if connect handler raised error we reject the + # connection + return False + else: + if run_async: + async def async_handler(): + return self.handlers[event](*args) + + task = self.start_background_task(async_handler) + task_reference_holder.add(task) + task.add_done_callback(task_reference_holder.discard) + return task + else: + try: + try: + ret = self.handlers[event](*args) + except TypeError: + if event == 'disconnect' and \ + len(args) == 1: # pragma: no branch + # legacy disconnect events do not have a reason + # argument + ret = self.handlers[event]() + else: # pragma: no cover + raise + except: + self.logger.exception(event + ' handler error') + if event == 'connect': + # if connect handler raised error we reject the + # connection + return False + return ret + + async def _read_loop_polling(self): + """Read packets by polling the Engine.IO server.""" + while self.state == 'connected' and self.write_loop_task: + self.logger.info( + 'Sending polling GET request to ' + self.base_url) + r = await self._send_request( + 'GET', self.base_url + self._get_url_timestamp(), + timeout=max(self.ping_interval, self.ping_timeout) + 5) + if r is None or isinstance(r, str): + self.logger.warning( + r or 'Connection refused by the server, aborting') + await self.queue.put(None) + break + if r.status < 200 or r.status >= 300: + self.logger.warning('Unexpected status code %s in server ' + 'response, aborting', r.status) + await self.queue.put(None) + break + try: + p = payload.Payload(encoded_payload=(await r.read()).decode( + 'utf-8')) + except ValueError: + self.logger.warning( + 'Unexpected packet from server, aborting') + await self.queue.put(None) + break + for pkt in p.packets: + await self._receive_packet(pkt) + + if self.write_loop_task: # pragma: no branch + self.logger.info('Waiting for write loop task to end') + await self.write_loop_task + if self.state == 'connected': + await self._trigger_event( + 'disconnect', self.reason.TRANSPORT_ERROR, run_async=False) + try: + base_client.connected_clients.remove(self) + except ValueError: # pragma: no cover + pass + await self._reset() + self.logger.info('Exiting read loop task') + + async def _read_loop_websocket(self): + """Read packets from the Engine.IO WebSocket connection.""" + while self.state == 'connected': + p = None + try: + p = await asyncio.wait_for( + self.ws.receive(), + timeout=self.ping_interval + self.ping_timeout) + if not isinstance(p.data, (str, bytes)): # pragma: no cover + self.logger.warning( + 'Server sent %s packet data %s, aborting', + 'close' if p.type in [aiohttp.WSMsgType.CLOSE, + aiohttp.WSMsgType.CLOSING] + else str(p.type), str(p.data)) + await self.queue.put(None) + break # the connection is broken + p = p.data + except asyncio.TimeoutError: + self.logger.warning( + 'Server has stopped communicating, aborting') + await self.queue.put(None) + break + except aiohttp.client_exceptions.ServerDisconnectedError: + self.logger.info( + 'Read loop: WebSocket connection was closed, aborting') + await self.queue.put(None) + break + except Exception as e: + self.logger.info( + 'Unexpected error receiving packet: "%s", aborting', + str(e)) + await self.queue.put(None) + break + try: + pkt = packet.Packet(encoded_packet=p) + except Exception as e: # pragma: no cover + self.logger.info( + 'Unexpected error decoding packet: "%s", aborting', str(e)) + await self.queue.put(None) + break + await self._receive_packet(pkt) + + if self.write_loop_task: # pragma: no branch + self.logger.info('Waiting for write loop task to end') + await self.write_loop_task + if self.state == 'connected': + await self._trigger_event( + 'disconnect', self.reason.TRANSPORT_ERROR, run_async=False) + try: + base_client.connected_clients.remove(self) + except ValueError: # pragma: no cover + pass + await self._reset() + self.logger.info('Exiting read loop task') + + async def _write_loop(self): + """This background task sends packages to the server as they are + pushed to the send queue. + """ + while self.state == 'connected': + # to simplify the timeout handling, use the maximum of the + # ping interval and ping timeout as timeout, with an extra 5 + # seconds grace period + timeout = max(self.ping_interval, self.ping_timeout) + 5 + packets = None + try: + packets = [await asyncio.wait_for(self.queue.get(), timeout)] + except (self.queue_empty, asyncio.TimeoutError): + self.logger.error('packet queue is empty, aborting') + break + except asyncio.CancelledError: # pragma: no cover + break + if packets == [None]: + self.queue.task_done() + packets = [] + else: + while True: + try: + packets.append(self.queue.get_nowait()) + except self.queue_empty: + break + if packets[-1] is None: + packets = packets[:-1] + self.queue.task_done() + break + if not packets: + # empty packet list returned -> connection closed + break + if self.current_transport == 'polling': + p = payload.Payload(packets=packets) + r = await self._send_request( + 'POST', self.base_url, body=p.encode(), + headers={'Content-Type': 'text/plain'}, + timeout=self.request_timeout) + for pkt in packets: + self.queue.task_done() + if r is None or isinstance(r, str): + self.logger.warning( + r or 'Connection refused by the server, aborting') + break + if r.status < 200 or r.status >= 300: + self.logger.warning('Unexpected status code %s in server ' + 'response, aborting', r.status) + self.write_loop_task = None + break + else: + # websocket + try: + for pkt in packets: + if pkt.binary: + await self.ws.send_bytes(pkt.encode()) + else: + await self.ws.send_str(pkt.encode()) + self.queue.task_done() + except (aiohttp.client_exceptions.ServerDisconnectedError, + BrokenPipeError, OSError): + self.logger.info( + 'Write loop: WebSocket connection was closed, ' + 'aborting') + break + self.logger.info('Exiting write loop task') diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_drivers/__init__.py b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_drivers/_websocket_wsgi.py b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/_websocket_wsgi.py new file mode 100644 index 0000000..aca30dc --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/_websocket_wsgi.py @@ -0,0 +1,34 @@ +import simple_websocket + + +class SimpleWebSocketWSGI: # pragma: no cover + """ + This wrapper class provides a threading WebSocket interface that is + compatible with eventlet's implementation. + """ + def __init__(self, handler, server, **kwargs): + self.app = handler + self.server_args = kwargs + + def __call__(self, environ, start_response): + self.ws = simple_websocket.Server(environ, **self.server_args) + ret = self.app(self) + if self.ws.mode == 'gunicorn': + raise StopIteration() + return ret + + def close(self): + if self.ws.connected: + self.ws.close() + + def send(self, message): + try: + return self.ws.send(message) + except simple_websocket.ConnectionClosed: + raise OSError() + + def wait(self): + try: + return self.ws.receive() + except simple_websocket.ConnectionClosed: + return None diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_drivers/aiohttp.py b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/aiohttp.py new file mode 100644 index 0000000..7c3440f --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/aiohttp.py @@ -0,0 +1,127 @@ +import asyncio +import sys +from urllib.parse import urlsplit + +from aiohttp.web import Response, WebSocketResponse + + +def create_route(app, engineio_server, engineio_endpoint): + """This function sets up the engine.io endpoint as a route for the + application. + + Note that both GET and POST requests must be hooked up on the engine.io + endpoint. + """ + app.router.add_get(engineio_endpoint, engineio_server.handle_request) + app.router.add_post(engineio_endpoint, engineio_server.handle_request) + app.router.add_route('OPTIONS', engineio_endpoint, + engineio_server.handle_request) + + +def translate_request(request): + """This function takes the arguments passed to the request handler and + uses them to generate a WSGI compatible environ dictionary. + """ + message = request._message + payload = request._payload + + uri_parts = urlsplit(message.path) + environ = { + 'wsgi.input': payload, + 'wsgi.errors': sys.stderr, + 'wsgi.version': (1, 0), + 'wsgi.async': True, + 'wsgi.multithread': False, + 'wsgi.multiprocess': False, + 'wsgi.run_once': False, + 'SERVER_SOFTWARE': 'aiohttp', + 'REQUEST_METHOD': message.method, + 'QUERY_STRING': uri_parts.query or '', + 'RAW_URI': message.path, + 'SERVER_PROTOCOL': 'HTTP/%s.%s' % message.version, + 'REMOTE_ADDR': '127.0.0.1', + 'REMOTE_PORT': '0', + 'SERVER_NAME': 'aiohttp', + 'SERVER_PORT': '0', + 'aiohttp.request': request + } + + for hdr_name, hdr_value in message.headers.items(): + hdr_name = hdr_name.upper() + if hdr_name == 'CONTENT-TYPE': + environ['CONTENT_TYPE'] = hdr_value + continue + elif hdr_name == 'CONTENT-LENGTH': + environ['CONTENT_LENGTH'] = hdr_value + continue + + key = 'HTTP_%s' % hdr_name.replace('-', '_') + if key in environ: + hdr_value = f'{environ[key]},{hdr_value}' + + environ[key] = hdr_value + + environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http') + + path_info = uri_parts.path + + environ['PATH_INFO'] = path_info + environ['SCRIPT_NAME'] = '' + + return environ + + +def make_response(status, headers, payload, environ): + """This function generates an appropriate response object for this async + mode. + """ + return Response(body=payload, status=int(status.split()[0]), + headers=headers) + + +class WebSocket: # pragma: no cover + """ + This wrapper class provides a aiohttp WebSocket interface that is + somewhat compatible with eventlet's implementation. + """ + def __init__(self, handler, server): + self.handler = handler + self._sock = None + + async def __call__(self, environ): + request = environ['aiohttp.request'] + self._sock = WebSocketResponse(max_msg_size=0) + await self._sock.prepare(request) + + self.environ = environ + await self.handler(self) + return self._sock + + async def close(self): + await self._sock.close() + + async def send(self, message): + if isinstance(message, bytes): + f = self._sock.send_bytes + else: + f = self._sock.send_str + if asyncio.iscoroutinefunction(f): + await f(message) + else: + f(message) + + async def wait(self): + msg = await self._sock.receive() + if not isinstance(msg.data, bytes) and \ + not isinstance(msg.data, str): + raise OSError() + return msg.data + + +_async = { + 'asyncio': True, + 'create_route': create_route, + 'translate_request': translate_request, + 'make_response': make_response, + 'websocket': WebSocket, +} diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_drivers/asgi.py b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/asgi.py new file mode 100644 index 0000000..ad7447e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/asgi.py @@ -0,0 +1,296 @@ +import os +import sys +import asyncio + +from engineio.static_files import get_static_file + + +class ASGIApp: + """ASGI application middleware for Engine.IO. + + This middleware dispatches traffic to an Engine.IO application. It can + also serve a list of static files to the client, or forward unrelated + HTTP traffic to another ASGI application. + + :param engineio_server: The Engine.IO server. Must be an instance of the + ``engineio.AsyncServer`` class. + :param static_files: A dictionary with static file mapping rules. See the + documentation for details on this argument. + :param other_asgi_app: A separate ASGI app that receives all other traffic. + :param engineio_path: The endpoint where the Engine.IO application should + be installed. The default value is appropriate for + most cases. With a value of ``None``, all incoming + traffic is directed to the Engine.IO server, with the + assumption that routing, if necessary, is handled by + a different layer. When this option is set to + ``None``, ``static_files`` and ``other_asgi_app`` are + ignored. + :param on_startup: function to be called on application startup; can be + coroutine + :param on_shutdown: function to be called on application shutdown; can be + coroutine + + Example usage:: + + import engineio + import uvicorn + + eio = engineio.AsyncServer() + app = engineio.ASGIApp(eio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'index.html'}, + '/index.html': {'content_type': 'text/html', + 'filename': 'index.html'}, + }) + uvicorn.run(app, '127.0.0.1', 5000) + """ + def __init__(self, engineio_server, other_asgi_app=None, + static_files=None, engineio_path='engine.io', + on_startup=None, on_shutdown=None): + self.engineio_server = engineio_server + self.other_asgi_app = other_asgi_app + self.engineio_path = engineio_path + if self.engineio_path is not None: + if not self.engineio_path.startswith('/'): + self.engineio_path = '/' + self.engineio_path + if not self.engineio_path.endswith('/'): + self.engineio_path += '/' + self.static_files = static_files or {} + self.on_startup = on_startup + self.on_shutdown = on_shutdown + + async def __call__(self, scope, receive, send): + if scope['type'] == 'lifespan': + await self.lifespan(scope, receive, send) + elif scope['type'] in ['http', 'websocket'] and ( + self.engineio_path is None + or self._ensure_trailing_slash(scope['path']).startswith( + self.engineio_path)): + await self.engineio_server.handle_request(scope, receive, send) + else: + static_file = get_static_file(scope['path'], self.static_files) \ + if scope['type'] == 'http' and self.static_files else None + if static_file and os.path.exists(static_file['filename']): + await self.serve_static_file(static_file, receive, send) + elif self.other_asgi_app is not None: + await self.other_asgi_app(scope, receive, send) + else: + await self.not_found(receive, send) + + async def serve_static_file(self, static_file, receive, + send): # pragma: no cover + event = await receive() + if event['type'] == 'http.request': + with open(static_file['filename'], 'rb') as f: + payload = f.read() + await send({'type': 'http.response.start', + 'status': 200, + 'headers': [(b'Content-Type', static_file[ + 'content_type'].encode('utf-8'))]}) + await send({'type': 'http.response.body', + 'body': payload}) + + async def lifespan(self, scope, receive, send): + if self.other_asgi_app is not None and self.on_startup is None and \ + self.on_shutdown is None: + # let the other ASGI app handle lifespan events + await self.other_asgi_app(scope, receive, send) + return + + while True: + event = await receive() + if event['type'] == 'lifespan.startup': + if self.on_startup: + try: + await self.on_startup() \ + if asyncio.iscoroutinefunction(self.on_startup) \ + else self.on_startup() + except: + await send({'type': 'lifespan.startup.failed'}) + return + await send({'type': 'lifespan.startup.complete'}) + elif event['type'] == 'lifespan.shutdown': + if self.on_shutdown: + try: + await self.on_shutdown() \ + if asyncio.iscoroutinefunction(self.on_shutdown) \ + else self.on_shutdown() + except: + await send({'type': 'lifespan.shutdown.failed'}) + return + await send({'type': 'lifespan.shutdown.complete'}) + return + + async def not_found(self, receive, send): + """Return a 404 Not Found error to the client.""" + await send({'type': 'http.response.start', + 'status': 404, + 'headers': [(b'Content-Type', b'text/plain')]}) + await send({'type': 'http.response.body', + 'body': b'Not Found'}) + + def _ensure_trailing_slash(self, path): + if not path.endswith('/'): + path += '/' + return path + + +async def translate_request(scope, receive, send): + class AwaitablePayload: # pragma: no cover + def __init__(self, payload): + self.payload = payload or b'' + + async def read(self, length=None): + if length is None: + r = self.payload + self.payload = b'' + else: + r = self.payload[:length] + self.payload = self.payload[length:] + return r + + event = await receive() + payload = b'' + if event['type'] == 'http.request': + payload += event.get('body') or b'' + while event.get('more_body'): + event = await receive() + if event['type'] == 'http.request': + payload += event.get('body') or b'' + elif event['type'] == 'websocket.connect': + pass + else: + return {} + + raw_uri = scope['path'] + query_string = '' + if 'query_string' in scope and scope['query_string']: + try: + query_string = scope['query_string'].decode('utf-8') + except UnicodeDecodeError: + pass + else: + raw_uri += '?' + query_string + environ = { + 'wsgi.input': AwaitablePayload(payload), + 'wsgi.errors': sys.stderr, + 'wsgi.version': (1, 0), + 'wsgi.async': True, + 'wsgi.multithread': False, + 'wsgi.multiprocess': False, + 'wsgi.run_once': False, + 'SERVER_SOFTWARE': 'asgi', + 'REQUEST_METHOD': scope.get('method', 'GET'), + 'PATH_INFO': scope['path'], + 'QUERY_STRING': query_string, + 'RAW_URI': raw_uri, + 'SCRIPT_NAME': '', + 'SERVER_PROTOCOL': 'HTTP/1.1', + 'REMOTE_ADDR': '127.0.0.1', + 'REMOTE_PORT': '0', + 'SERVER_NAME': 'asgi', + 'SERVER_PORT': '0', + 'asgi.receive': receive, + 'asgi.send': send, + 'asgi.scope': scope, + } + + for hdr_name, hdr_value in scope['headers']: + try: + hdr_name = hdr_name.upper().decode('utf-8') + hdr_value = hdr_value.decode('utf-8') + except UnicodeDecodeError: + # skip header if it cannot be decoded + continue + if hdr_name == 'CONTENT-TYPE': + environ['CONTENT_TYPE'] = hdr_value + continue + elif hdr_name == 'CONTENT-LENGTH': + environ['CONTENT_LENGTH'] = hdr_value + continue + + key = 'HTTP_%s' % hdr_name.replace('-', '_') + if key in environ: + hdr_value = f'{environ[key]},{hdr_value}' + + environ[key] = hdr_value + + environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http') + return environ + + +async def make_response(status, headers, payload, environ): + headers = [(h[0].encode('utf-8'), h[1].encode('utf-8')) for h in headers] + if environ['asgi.scope']['type'] == 'websocket': + if status.startswith('200 '): + await environ['asgi.send']({'type': 'websocket.accept', + 'headers': headers}) + else: + if payload: + reason = payload.decode('utf-8') \ + if isinstance(payload, bytes) else str(payload) + await environ['asgi.send']({'type': 'websocket.close', + 'reason': reason}) + else: + await environ['asgi.send']({'type': 'websocket.close'}) + return + + await environ['asgi.send']({'type': 'http.response.start', + 'status': int(status.split(' ')[0]), + 'headers': headers}) + await environ['asgi.send']({'type': 'http.response.body', + 'body': payload}) + + +class WebSocket: # pragma: no cover + """ + This wrapper class provides an asgi WebSocket interface that is + somewhat compatible with eventlet's implementation. + """ + def __init__(self, handler, server): + self.handler = handler + self.asgi_receive = None + self.asgi_send = None + + async def __call__(self, environ): + self.asgi_receive = environ['asgi.receive'] + self.asgi_send = environ['asgi.send'] + await self.asgi_send({'type': 'websocket.accept'}) + await self.handler(self) + return '' # send nothing as response + + async def close(self): + try: + await self.asgi_send({'type': 'websocket.close'}) + except Exception: + # if the socket is already close we don't care + pass + + async def send(self, message): + msg_bytes = None + msg_text = None + if isinstance(message, bytes): + msg_bytes = message + else: + msg_text = message + await self.asgi_send({'type': 'websocket.send', + 'bytes': msg_bytes, + 'text': msg_text}) + + async def wait(self): + event = await self.asgi_receive() + if event['type'] != 'websocket.receive': + raise OSError() + if event.get('bytes', None) is not None: + return event['bytes'] + elif event.get('text', None) is not None: + return event['text'] + else: # pragma: no cover + raise OSError() + + +_async = { + 'asyncio': True, + 'translate_request': translate_request, + 'make_response': make_response, + 'websocket': WebSocket, +} diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_drivers/eventlet.py b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/eventlet.py new file mode 100644 index 0000000..6361c4d --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/eventlet.py @@ -0,0 +1,52 @@ +from eventlet.green.threading import Event +from eventlet import queue, sleep, spawn +from eventlet.websocket import WebSocketWSGI as _WebSocketWSGI + + +class EventletThread: # pragma: no cover + """Thread class that uses eventlet green threads. + + Eventlet's own Thread class has a strange bug that causes _DummyThread + objects to be created and leaked, since they are never garbage collected. + """ + def __init__(self, target, args=None, kwargs=None): + self.target = target + self.args = args or () + self.kwargs = kwargs or {} + self.g = None + + def start(self): + self.g = spawn(self.target, *self.args, **self.kwargs) + + def join(self): + if self.g: + return self.g.wait() + + +class WebSocketWSGI(_WebSocketWSGI): # pragma: no cover + def __init__(self, handler, server): + try: + super().__init__( + handler, max_frame_length=int(server.max_http_buffer_size)) + except TypeError: # pragma: no cover + # older versions of eventlet do not support a max frame size + super().__init__(handler) + self._sock = None + + def __call__(self, environ, start_response): + if 'eventlet.input' not in environ: + raise RuntimeError('You need to use the eventlet server. ' + 'See the Deployment section of the ' + 'documentation for more information.') + self._sock = environ['eventlet.input'].get_socket() + return super().__call__(environ, start_response) + + +_async = { + 'thread': EventletThread, + 'queue': queue.Queue, + 'queue_empty': queue.Empty, + 'event': Event, + 'websocket': WebSocketWSGI, + 'sleep': sleep, +} diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_drivers/gevent.py b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/gevent.py new file mode 100644 index 0000000..db284a5 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/gevent.py @@ -0,0 +1,83 @@ +import gevent +from gevent import queue +from gevent.event import Event +try: + # use gevent-websocket if installed + import geventwebsocket # noqa + SimpleWebSocketWSGI = None +except ImportError: # pragma: no cover + # fallback to simple_websocket when gevent-websocket is not installed + from engineio.async_drivers._websocket_wsgi import SimpleWebSocketWSGI + + +class Thread(gevent.Greenlet): # pragma: no cover + """ + This wrapper class provides gevent Greenlet interface that is compatible + with the standard library's Thread class. + """ + def __init__(self, target, args=[], kwargs={}): + super().__init__(target, *args, **kwargs) + + def _run(self): + return self.run() + + +if SimpleWebSocketWSGI is not None: + class WebSocketWSGI(SimpleWebSocketWSGI): # pragma: no cover + """ + This wrapper class provides a gevent WebSocket interface that is + compatible with eventlet's implementation, using the simple-websocket + package. + """ + def __init__(self, handler, server): + # to avoid the requirement that the standard library is + # monkey-patched, here we pass the gevent versions of the + # concurrency and networking classes required by simple-websocket + import gevent.event + import gevent.selectors + super().__init__(handler, server, + thread_class=Thread, + event_class=gevent.event.Event, + selector_class=gevent.selectors.DefaultSelector) +else: + class WebSocketWSGI: # pragma: no cover + """ + This wrapper class provides a gevent WebSocket interface that is + compatible with eventlet's implementation, using the gevent-websocket + package. + """ + def __init__(self, handler, server): + self.app = handler + + def __call__(self, environ, start_response): + if 'wsgi.websocket' not in environ: + raise RuntimeError('The gevent-websocket server is not ' + 'configured appropriately. ' + 'See the Deployment section of the ' + 'documentation for more information.') + self._sock = environ['wsgi.websocket'] + self.environ = environ + self.version = self._sock.version + self.path = self._sock.path + self.origin = self._sock.origin + self.protocol = self._sock.protocol + return self.app(self) + + def close(self): + return self._sock.close() + + def send(self, message): + return self._sock.send(message) + + def wait(self): + return self._sock.receive() + + +_async = { + 'thread': Thread, + 'queue': queue.JoinableQueue, + 'queue_empty': queue.Empty, + 'event': Event, + 'websocket': WebSocketWSGI, + 'sleep': gevent.sleep, +} diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_drivers/gevent_uwsgi.py b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/gevent_uwsgi.py new file mode 100644 index 0000000..b5ccefc --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/gevent_uwsgi.py @@ -0,0 +1,168 @@ +import gevent +from gevent import queue +from gevent.event import Event +from gevent import selectors +import uwsgi +_websocket_available = hasattr(uwsgi, 'websocket_handshake') + + +class Thread(gevent.Greenlet): # pragma: no cover + """ + This wrapper class provides gevent Greenlet interface that is compatible + with the standard library's Thread class. + """ + def __init__(self, target, args=[], kwargs={}): + super().__init__(target, *args, **kwargs) + + def _run(self): + return self.run() + + +class uWSGIWebSocket: # pragma: no cover + """ + This wrapper class provides a uWSGI WebSocket interface that is + compatible with eventlet's implementation. + """ + def __init__(self, handler, server): + self.app = handler + self._sock = None + self.received_messages = [] + + def __call__(self, environ, start_response): + self._sock = uwsgi.connection_fd() + self.environ = environ + + uwsgi.websocket_handshake() + + self._req_ctx = None + if hasattr(uwsgi, 'request_context'): + # uWSGI >= 2.1.x with support for api access across-greenlets + self._req_ctx = uwsgi.request_context() + else: + # use event and queue for sending messages + self._event = Event() + self._send_queue = queue.Queue() + + # spawn a select greenlet + def select_greenlet_runner(fd, event): + """Sets event when data becomes available to read on fd.""" + sel = selectors.DefaultSelector() + sel.register(fd, selectors.EVENT_READ) + try: + while True: + sel.select() + event.set() + except gevent.GreenletExit: + sel.unregister(fd) + self._select_greenlet = gevent.spawn( + select_greenlet_runner, + self._sock, + self._event) + + self.app(self) + uwsgi.disconnect() + return '' # send nothing as response + + def close(self): + """Disconnects uWSGI from the client.""" + if self._req_ctx is None: + # better kill it here in case wait() is not called again + self._select_greenlet.kill() + self._event.set() + + def _send(self, msg): + """Transmits message either in binary or UTF-8 text mode, + depending on its type.""" + if isinstance(msg, bytes): + method = uwsgi.websocket_send_binary + else: + method = uwsgi.websocket_send + if self._req_ctx is not None: + method(msg, request_context=self._req_ctx) + else: + method(msg) + + def _decode_received(self, msg): + """Returns either bytes or str, depending on message type.""" + if not isinstance(msg, bytes): + # already decoded - do nothing + return msg + # only decode from utf-8 if message is not binary data + type = ord(msg[0:1]) + if type >= 48: # no binary + return msg.decode('utf-8') + # binary message, don't try to decode + return msg + + def send(self, msg): + """Queues a message for sending. Real transmission is done in + wait method. + Sends directly if uWSGI version is new enough.""" + if self._req_ctx is not None: + self._send(msg) + else: + self._send_queue.put(msg) + self._event.set() + + def wait(self): + """Waits and returns received messages. + If running in compatibility mode for older uWSGI versions, + it also sends messages that have been queued by send(). + A return value of None means that connection was closed. + This must be called repeatedly. For uWSGI < 2.1.x it must + be called from the main greenlet.""" + while True: + if self._req_ctx is not None: + try: + msg = uwsgi.websocket_recv(request_context=self._req_ctx) + except OSError: # connection closed + self.close() + return None + return self._decode_received(msg) + else: + if self.received_messages: + return self.received_messages.pop(0) + + # we wake up at least every 3 seconds to let uWSGI + # do its ping/ponging + event_set = self._event.wait(timeout=3) + if event_set: + self._event.clear() + # maybe there is something to send + msgs = [] + while True: + try: + msgs.append(self._send_queue.get(block=False)) + except gevent.queue.Empty: + break + for msg in msgs: + try: + self._send(msg) + except OSError: + self.close() + return None + # maybe there is something to receive, if not, at least + # ensure uWSGI does its ping/ponging + while True: + try: + msg = uwsgi.websocket_recv_nb() + except OSError: # connection closed + self.close() + return None + if msg: # message available + self.received_messages.append( + self._decode_received(msg)) + else: + break + if self.received_messages: + return self.received_messages.pop(0) + + +_async = { + 'thread': Thread, + 'queue': queue.JoinableQueue, + 'queue_empty': queue.Empty, + 'event': Event, + 'websocket': uWSGIWebSocket if _websocket_available else None, + 'sleep': gevent.sleep, +} diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_drivers/sanic.py b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/sanic.py new file mode 100644 index 0000000..4d6a5b8 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/sanic.py @@ -0,0 +1,148 @@ +import sys +from urllib.parse import urlsplit + +try: # pragma: no cover + from sanic.response import HTTPResponse + try: + from sanic.server.protocols.websocket_protocol import WebSocketProtocol + except ImportError: + from sanic.websocket import WebSocketProtocol +except ImportError: + HTTPResponse = None + WebSocketProtocol = None + + +def create_route(app, engineio_server, engineio_endpoint): # pragma: no cover + """This function sets up the engine.io endpoint as a route for the + application. + + Note that both GET and POST requests must be hooked up on the engine.io + endpoint. + """ + app.add_route(engineio_server.handle_request, engineio_endpoint, + methods=['GET', 'POST', 'OPTIONS']) + try: + app.enable_websocket() + except AttributeError: + # ignore, this version does not support websocket + pass + + +def translate_request(request): # pragma: no cover + """This function takes the arguments passed to the request handler and + uses them to generate a WSGI compatible environ dictionary. + """ + class AwaitablePayload: + def __init__(self, payload): + self.payload = payload or b'' + + async def read(self, length=None): + if length is None: + r = self.payload + self.payload = b'' + else: + r = self.payload[:length] + self.payload = self.payload[length:] + return r + + uri_parts = urlsplit(request.url) + environ = { + 'wsgi.input': AwaitablePayload(request.body), + 'wsgi.errors': sys.stderr, + 'wsgi.version': (1, 0), + 'wsgi.async': True, + 'wsgi.multithread': False, + 'wsgi.multiprocess': False, + 'wsgi.run_once': False, + 'SERVER_SOFTWARE': 'sanic', + 'REQUEST_METHOD': request.method, + 'QUERY_STRING': uri_parts.query or '', + 'RAW_URI': request.url, + 'SERVER_PROTOCOL': 'HTTP/' + request.version, + 'REMOTE_ADDR': '127.0.0.1', + 'REMOTE_PORT': '0', + 'SERVER_NAME': 'sanic', + 'SERVER_PORT': '0', + 'sanic.request': request + } + + for hdr_name, hdr_value in request.headers.items(): + hdr_name = hdr_name.upper() + if hdr_name == 'CONTENT-TYPE': + environ['CONTENT_TYPE'] = hdr_value + continue + elif hdr_name == 'CONTENT-LENGTH': + environ['CONTENT_LENGTH'] = hdr_value + continue + + key = 'HTTP_%s' % hdr_name.replace('-', '_') + if key in environ: + hdr_value = f'{environ[key]},{hdr_value}' + + environ[key] = hdr_value + + environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http') + + path_info = uri_parts.path + + environ['PATH_INFO'] = path_info + environ['SCRIPT_NAME'] = '' + + return environ + + +def make_response(status, headers, payload, environ): # pragma: no cover + """This function generates an appropriate response object for this async + mode. + """ + headers_dict = {} + content_type = None + for h in headers: + if h[0].lower() == 'content-type': + content_type = h[1] + else: + headers_dict[h[0]] = h[1] + return HTTPResponse(body=payload, content_type=content_type, + status=int(status.split()[0]), headers=headers_dict) + + +class WebSocket: # pragma: no cover + """ + This wrapper class provides a sanic WebSocket interface that is + somewhat compatible with eventlet's implementation. + """ + def __init__(self, handler, server): + self.handler = handler + self.server = server + self._sock = None + + async def __call__(self, environ): + request = environ['sanic.request'] + protocol = request.transport.get_protocol() + self._sock = await protocol.websocket_handshake(request) + + self.environ = environ + await self.handler(self) + return self.server._ok() + + async def close(self): + await self._sock.close() + + async def send(self, message): + await self._sock.send(message) + + async def wait(self): + data = await self._sock.recv() + if not isinstance(data, bytes) and \ + not isinstance(data, str): + raise OSError() + return data + + +_async = { + 'asyncio': True, + 'create_route': create_route, + 'translate_request': translate_request, + 'make_response': make_response, + 'websocket': WebSocket if WebSocketProtocol else None, +} diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_drivers/threading.py b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/threading.py new file mode 100644 index 0000000..1615579 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/threading.py @@ -0,0 +1,19 @@ +import queue +import threading +import time +from engineio.async_drivers._websocket_wsgi import SimpleWebSocketWSGI + + +class DaemonThread(threading.Thread): # pragma: no cover + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs, daemon=True) + + +_async = { + 'thread': DaemonThread, + 'queue': queue.Queue, + 'queue_empty': queue.Empty, + 'event': threading.Event, + 'websocket': SimpleWebSocketWSGI, + 'sleep': time.sleep, +} diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_drivers/tornado.py b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/tornado.py new file mode 100644 index 0000000..abb1e2b --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_drivers/tornado.py @@ -0,0 +1,182 @@ +import asyncio +import sys +from urllib.parse import urlsplit +from .. import exceptions + +import tornado.web +import tornado.websocket + + +def get_tornado_handler(engineio_server): + class Handler(tornado.websocket.WebSocketHandler): # pragma: no cover + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if isinstance(engineio_server.cors_allowed_origins, str): + if engineio_server.cors_allowed_origins == '*': + self.allowed_origins = None + else: + self.allowed_origins = [ + engineio_server.cors_allowed_origins] + else: + self.allowed_origins = engineio_server.cors_allowed_origins + self.receive_queue = asyncio.Queue() + + async def get(self, *args, **kwargs): + if self.request.headers.get('Upgrade', '').lower() == 'websocket': + ret = super().get(*args, **kwargs) + if asyncio.iscoroutine(ret): + await ret + else: + await engineio_server.handle_request(self) + + async def open(self, *args, **kwargs): + # this is the handler for the websocket request + asyncio.ensure_future(engineio_server.handle_request(self)) + + async def post(self, *args, **kwargs): + await engineio_server.handle_request(self) + + async def options(self, *args, **kwargs): + await engineio_server.handle_request(self) + + async def on_message(self, message): + await self.receive_queue.put(message) + + async def get_next_message(self): + return await self.receive_queue.get() + + def on_close(self): + self.receive_queue.put_nowait(None) + + def check_origin(self, origin): + if self.allowed_origins is None or origin in self.allowed_origins: + return True + return super().check_origin(origin) + + def get_compression_options(self): + # enable compression + return {} + + return Handler + + +def translate_request(handler): + """This function takes the arguments passed to the request handler and + uses them to generate a WSGI compatible environ dictionary. + """ + class AwaitablePayload: + def __init__(self, payload): + self.payload = payload or b'' + + async def read(self, length=None): + if length is None: + r = self.payload + self.payload = b'' + else: + r = self.payload[:length] + self.payload = self.payload[length:] + return r + + payload = handler.request.body + + uri_parts = urlsplit(handler.request.path) + full_uri = handler.request.path + if handler.request.query: # pragma: no cover + full_uri += '?' + handler.request.query + environ = { + 'wsgi.input': AwaitablePayload(payload), + 'wsgi.errors': sys.stderr, + 'wsgi.version': (1, 0), + 'wsgi.async': True, + 'wsgi.multithread': False, + 'wsgi.multiprocess': False, + 'wsgi.run_once': False, + 'SERVER_SOFTWARE': 'aiohttp', + 'REQUEST_METHOD': handler.request.method, + 'QUERY_STRING': handler.request.query or '', + 'RAW_URI': full_uri, + 'SERVER_PROTOCOL': 'HTTP/%s' % handler.request.version, + 'REMOTE_ADDR': '127.0.0.1', + 'REMOTE_PORT': '0', + 'SERVER_NAME': 'aiohttp', + 'SERVER_PORT': '0', + 'tornado.handler': handler + } + + for hdr_name, hdr_value in handler.request.headers.items(): + hdr_name = hdr_name.upper() + if hdr_name == 'CONTENT-TYPE': + environ['CONTENT_TYPE'] = hdr_value + continue + elif hdr_name == 'CONTENT-LENGTH': + environ['CONTENT_LENGTH'] = hdr_value + continue + + key = 'HTTP_%s' % hdr_name.replace('-', '_') + environ[key] = hdr_value + + environ['wsgi.url_scheme'] = environ.get('HTTP_X_FORWARDED_PROTO', 'http') + + path_info = uri_parts.path + + environ['PATH_INFO'] = path_info + environ['SCRIPT_NAME'] = '' + + return environ + + +def make_response(status, headers, payload, environ): + """This function generates an appropriate response object for this async + mode. + """ + tornado_handler = environ['tornado.handler'] + try: + tornado_handler.set_status(int(status.split()[0])) + except RuntimeError: # pragma: no cover + # for websocket connections Tornado does not accept a response, since + # it already emitted the 101 status code + return + for header, value in headers: + tornado_handler.set_header(header, value) + tornado_handler.write(payload) + tornado_handler.finish() + + +class WebSocket: # pragma: no cover + """ + This wrapper class provides a tornado WebSocket interface that is + somewhat compatible with eventlet's implementation. + """ + def __init__(self, handler, server): + self.handler = handler + self.tornado_handler = None + + async def __call__(self, environ): + self.tornado_handler = environ['tornado.handler'] + self.environ = environ + await self.handler(self) + + async def close(self): + self.tornado_handler.close() + + async def send(self, message): + try: + self.tornado_handler.write_message( + message, binary=isinstance(message, bytes)) + except tornado.websocket.WebSocketClosedError: + raise exceptions.EngineIOError() + + async def wait(self): + msg = await self.tornado_handler.get_next_message() + if not isinstance(msg, bytes) and \ + not isinstance(msg, str): + raise OSError() + return msg + + +_async = { + 'asyncio': True, + 'translate_request': translate_request, + 'make_response': make_response, + 'websocket': WebSocket, +} diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_server.py b/tapdown/lib/python3.11/site-packages/engineio/async_server.py new file mode 100644 index 0000000..c417067 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_server.py @@ -0,0 +1,611 @@ +import asyncio +import urllib + +from . import base_server +from . import exceptions +from . import packet +from . import async_socket + +# this set is used to keep references to background tasks to prevent them from +# being garbage collected mid-execution. Solution taken from +# https://docs.python.org/3/library/asyncio-task.html#asyncio.create_task +task_reference_holder = set() + + +class AsyncServer(base_server.BaseServer): + """An Engine.IO server for asyncio. + + This class implements a fully compliant Engine.IO web server with support + for websocket and long-polling transports, compatible with the asyncio + framework on Python 3.5 or newer. + + :param async_mode: The asynchronous model to use. See the Deployment + section in the documentation for a description of the + available options. Valid async modes are "aiohttp", + "sanic", "tornado" and "asgi". If this argument is not + given, "aiohttp" is tried first, followed by "sanic", + "tornado", and finally "asgi". The first async mode that + has all its dependencies installed is the one that is + chosen. + :param ping_interval: The interval in seconds at which the server pings + the client. The default is 25 seconds. For advanced + control, a two element tuple can be given, where + the first number is the ping interval and the second + is a grace period added by the server. + :param ping_timeout: The time in seconds that the client waits for the + server to respond before disconnecting. The default + is 20 seconds. + :param max_http_buffer_size: The maximum size that is accepted for incoming + messages. The default is 1,000,000 bytes. In + spite of its name, the value set in this + argument is enforced for HTTP long-polling and + WebSocket connections. + :param allow_upgrades: Whether to allow transport upgrades or not. + :param http_compression: Whether to compress packages when using the + polling transport. + :param compression_threshold: Only compress messages when their byte size + is greater than this value. + :param cookie: If set to a string, it is the name of the HTTP cookie the + server sends back tot he client containing the client + session id. If set to a dictionary, the ``'name'`` key + contains the cookie name and other keys define cookie + attributes, where the value of each attribute can be a + string, a callable with no arguments, or a boolean. If set + to ``None`` (the default), a cookie is not sent to the + client. + :param cors_allowed_origins: Origin or list of origins that are allowed to + connect to this server. Only the same origin + is allowed by default. Set this argument to + ``'*'`` or ``['*']`` to allow all origins, or + to ``[]`` to disable CORS handling. + :param cors_credentials: Whether credentials (cookies, authentication) are + allowed in requests to this server. + :param logger: To enable logging set to ``True`` or pass a logger object to + use. To disable logging set to ``False``. Note that fatal + errors are logged even when ``logger`` is ``False``. + :param json: An alternative json module to use for encoding and decoding + packets. Custom json modules must have ``dumps`` and ``loads`` + functions that are compatible with the standard library + versions. + :param async_handlers: If set to ``True``, run message event handlers in + non-blocking threads. To run handlers synchronously, + set to ``False``. The default is ``True``. + :param monitor_clients: If set to ``True``, a background task will ensure + inactive clients are closed. Set to ``False`` to + disable the monitoring task (not recommended). The + default is ``True``. + :param transports: The list of allowed transports. Valid transports + are ``'polling'`` and ``'websocket'``. Defaults to + ``['polling', 'websocket']``. + :param kwargs: Reserved for future extensions, any additional parameters + given as keyword arguments will be silently ignored. + """ + def is_asyncio_based(self): + return True + + def async_modes(self): + return ['aiohttp', 'sanic', 'tornado', 'asgi'] + + def attach(self, app, engineio_path='engine.io'): + """Attach the Engine.IO server to an application.""" + engineio_path = engineio_path.strip('/') + self._async['create_route'](app, self, f'/{engineio_path}/') + + async def send(self, sid, data): + """Send a message to a client. + + :param sid: The session id of the recipient client. + :param data: The data to send to the client. Data can be of type + ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` + or ``dict``, the data will be serialized as JSON. + + Note: this method is a coroutine. + """ + await self.send_packet(sid, packet.Packet(packet.MESSAGE, data=data)) + + async def send_packet(self, sid, pkt): + """Send a raw packet to a client. + + :param sid: The session id of the recipient client. + :param pkt: The packet to send to the client. + + Note: this method is a coroutine. + """ + try: + socket = self._get_socket(sid) + except KeyError: + # the socket is not available + self.logger.warning('Cannot send to sid %s', sid) + return + await socket.send(pkt) + + async def get_session(self, sid): + """Return the user session for a client. + + :param sid: The session id of the client. + + The return value is a dictionary. Modifications made to this + dictionary are not guaranteed to be preserved. If you want to modify + the user session, use the ``session`` context manager instead. + """ + socket = self._get_socket(sid) + return socket.session + + async def save_session(self, sid, session): + """Store the user session for a client. + + :param sid: The session id of the client. + :param session: The session dictionary. + """ + socket = self._get_socket(sid) + socket.session = session + + def session(self, sid): + """Return the user session for a client with context manager syntax. + + :param sid: The session id of the client. + + This is a context manager that returns the user session dictionary for + the client. Any changes that are made to this dictionary inside the + context manager block are saved back to the session. Example usage:: + + @eio.on('connect') + def on_connect(sid, environ): + username = authenticate_user(environ) + if not username: + return False + with eio.session(sid) as session: + session['username'] = username + + @eio.on('message') + def on_message(sid, msg): + async with eio.session(sid) as session: + print('received message from ', session['username']) + """ + class _session_context_manager: + def __init__(self, server, sid): + self.server = server + self.sid = sid + self.session = None + + async def __aenter__(self): + self.session = await self.server.get_session(sid) + return self.session + + async def __aexit__(self, *args): + await self.server.save_session(sid, self.session) + + return _session_context_manager(self, sid) + + async def disconnect(self, sid=None): + """Disconnect a client. + + :param sid: The session id of the client to close. If this parameter + is not given, then all clients are closed. + + Note: this method is a coroutine. + """ + if sid is not None: + try: + socket = self._get_socket(sid) + except KeyError: # pragma: no cover + # the socket was already closed or gone + pass + else: + await socket.close(reason=self.reason.SERVER_DISCONNECT) + if sid in self.sockets: # pragma: no cover + del self.sockets[sid] + else: + await asyncio.wait([ + asyncio.create_task(client.close( + reason=self.reason.SERVER_DISCONNECT)) + for client in self.sockets.values() + ]) + self.sockets = {} + + async def handle_request(self, *args, **kwargs): + """Handle an HTTP request from the client. + + This is the entry point of the Engine.IO application. This function + returns the HTTP response to deliver to the client. + + Note: this method is a coroutine. + """ + translate_request = self._async['translate_request'] + if asyncio.iscoroutinefunction(translate_request): + environ = await translate_request(*args, **kwargs) + else: + environ = translate_request(*args, **kwargs) + + if self.cors_allowed_origins != []: + # Validate the origin header if present + # This is important for WebSocket more than for HTTP, since + # browsers only apply CORS controls to HTTP. + origin = environ.get('HTTP_ORIGIN') + if origin: + allowed_origins = self._cors_allowed_origins(environ) + if allowed_origins is not None and origin not in \ + allowed_origins: + self._log_error_once( + origin + ' is not an accepted origin.', 'bad-origin') + return await self._make_response( + self._bad_request( + origin + ' is not an accepted origin.'), + environ) + + method = environ['REQUEST_METHOD'] + query = urllib.parse.parse_qs(environ.get('QUERY_STRING', '')) + + sid = query['sid'][0] if 'sid' in query else None + jsonp = False + jsonp_index = None + + # make sure the client uses an allowed transport + transport = query.get('transport', ['polling'])[0] + if transport not in self.transports: + self._log_error_once('Invalid transport', 'bad-transport') + return await self._make_response( + self._bad_request('Invalid transport'), environ) + + # make sure the client speaks a compatible Engine.IO version + sid = query['sid'][0] if 'sid' in query else None + if sid is None and query.get('EIO') != ['4']: + self._log_error_once( + 'The client is using an unsupported version of the Socket.IO ' + 'or Engine.IO protocols', 'bad-version' + ) + return await self._make_response(self._bad_request( + 'The client is using an unsupported version of the Socket.IO ' + 'or Engine.IO protocols' + ), environ) + + if 'j' in query: + jsonp = True + try: + jsonp_index = int(query['j'][0]) + except (ValueError, KeyError, IndexError): + # Invalid JSONP index number + pass + + if jsonp and jsonp_index is None: + self._log_error_once('Invalid JSONP index number', + 'bad-jsonp-index') + r = self._bad_request('Invalid JSONP index number') + elif method == 'GET': + upgrade_header = environ.get('HTTP_UPGRADE').lower() \ + if 'HTTP_UPGRADE' in environ else None + if sid is None: + # transport must be one of 'polling' or 'websocket'. + # if 'websocket', the HTTP_UPGRADE header must match. + if transport == 'polling' \ + or transport == upgrade_header == 'websocket': + r = await self._handle_connect(environ, transport, + jsonp_index) + else: + self._log_error_once('Invalid websocket upgrade', + 'bad-upgrade') + r = self._bad_request('Invalid websocket upgrade') + else: + if sid not in self.sockets: + self._log_error_once(f'Invalid session {sid}', 'bad-sid') + r = self._bad_request(f'Invalid session {sid}') + else: + try: + socket = self._get_socket(sid) + except KeyError as e: # pragma: no cover + self._log_error_once(f'{e} {sid}', 'bad-sid') + r = self._bad_request(f'{e} {sid}') + else: + if self.transport(sid) != transport and \ + transport != upgrade_header: + self._log_error_once( + f'Invalid transport for session {sid}', + 'bad-transport') + r = self._bad_request('Invalid transport') + else: + try: + packets = await socket.handle_get_request( + environ) + if isinstance(packets, list): + r = self._ok(packets, + jsonp_index=jsonp_index) + else: + r = packets + except exceptions.EngineIOError: + if sid in self.sockets: # pragma: no cover + await self.disconnect(sid) + r = self._bad_request() + if sid in self.sockets and \ + self.sockets[sid].closed: + del self.sockets[sid] + elif method == 'POST': + if sid is None or sid not in self.sockets: + self._log_error_once(f'Invalid session {sid}', 'bad-sid') + r = self._bad_request(f'Invalid session {sid}') + else: + socket = self._get_socket(sid) + try: + await socket.handle_post_request(environ) + r = self._ok(jsonp_index=jsonp_index) + except exceptions.EngineIOError: + if sid in self.sockets: # pragma: no cover + await self.disconnect(sid) + r = self._bad_request() + except: # pragma: no cover + # for any other unexpected errors, we log the error + # and keep going + self.logger.exception('post request handler error') + r = self._ok(jsonp_index=jsonp_index) + elif method == 'OPTIONS': + r = self._ok() + else: + self.logger.warning('Method %s not supported', method) + r = self._method_not_found() + if not isinstance(r, dict): + return r + if self.http_compression and \ + len(r['response']) >= self.compression_threshold: + encodings = [e.split(';')[0].strip() for e in + environ.get('HTTP_ACCEPT_ENCODING', '').split(',')] + for encoding in encodings: + if encoding in self.compression_methods: + r['response'] = \ + getattr(self, '_' + encoding)(r['response']) + r['headers'] += [('Content-Encoding', encoding)] + break + return await self._make_response(r, environ) + + async def shutdown(self): + """Stop Socket.IO background tasks. + + This method stops background activity initiated by the Socket.IO + server. It must be called before shutting down the web server. + """ + self.logger.info('Socket.IO is shutting down') + if self.service_task_event: # pragma: no cover + self.service_task_event.set() + await self.service_task_handle + self.service_task_handle = None + + def start_background_task(self, target, *args, **kwargs): + """Start a background task using the appropriate async model. + + This is a utility function that applications can use to start a + background task using the method that is compatible with the + selected async mode. + + :param target: the target function to execute. + :param args: arguments to pass to the function. + :param kwargs: keyword arguments to pass to the function. + + The return value is a ``asyncio.Task`` object. + """ + return asyncio.ensure_future(target(*args, **kwargs)) + + async def sleep(self, seconds=0): + """Sleep for the requested amount of time using the appropriate async + model. + + This is a utility function that applications can use to put a task to + sleep without having to worry about using the correct call for the + selected async mode. + + Note: this method is a coroutine. + """ + return await asyncio.sleep(seconds) + + def create_queue(self, *args, **kwargs): + """Create a queue object using the appropriate async model. + + This is a utility function that applications can use to create a queue + without having to worry about using the correct call for the selected + async mode. For asyncio based async modes, this returns an instance of + ``asyncio.Queue``. + """ + return asyncio.Queue(*args, **kwargs) + + def get_queue_empty_exception(self): + """Return the queue empty exception for the appropriate async model. + + This is a utility function that applications can use to work with a + queue without having to worry about using the correct call for the + selected async mode. For asyncio based async modes, this returns an + instance of ``asyncio.QueueEmpty``. + """ + return asyncio.QueueEmpty + + def create_event(self, *args, **kwargs): + """Create an event object using the appropriate async model. + + This is a utility function that applications can use to create an + event without having to worry about using the correct call for the + selected async mode. For asyncio based async modes, this returns + an instance of ``asyncio.Event``. + """ + return asyncio.Event(*args, **kwargs) + + async def _make_response(self, response_dict, environ): + cors_headers = self._cors_headers(environ) + make_response = self._async['make_response'] + if asyncio.iscoroutinefunction(make_response): + response = await make_response( + response_dict['status'], + response_dict['headers'] + cors_headers, + response_dict['response'], environ) + else: + response = make_response( + response_dict['status'], + response_dict['headers'] + cors_headers, + response_dict['response'], environ) + return response + + async def _handle_connect(self, environ, transport, jsonp_index=None): + """Handle a client connection request.""" + if self.start_service_task: + # start the service task to monitor connected clients + self.start_service_task = False + self.service_task_handle = self.start_background_task( + self._service_task) + + sid = self.generate_id() + s = async_socket.AsyncSocket(self, sid) + self.sockets[sid] = s + + pkt = packet.Packet(packet.OPEN, { + 'sid': sid, + 'upgrades': self._upgrades(sid, transport), + 'pingTimeout': int(self.ping_timeout * 1000), + 'pingInterval': int( + self.ping_interval + self.ping_interval_grace_period) * 1000, + 'maxPayload': self.max_http_buffer_size, + }) + await s.send(pkt) + s.schedule_ping() + + ret = await self._trigger_event('connect', sid, environ, + run_async=False) + if ret is not None and ret is not True: + del self.sockets[sid] + self.logger.warning('Application rejected connection') + return self._unauthorized(ret or None) + + if transport == 'websocket': + ret = await s.handle_get_request(environ) + if s.closed and sid in self.sockets: + # websocket connection ended, so we are done + del self.sockets[sid] + return ret + else: + s.connected = True + headers = None + if self.cookie: + if isinstance(self.cookie, dict): + headers = [( + 'Set-Cookie', + self._generate_sid_cookie(sid, self.cookie) + )] + else: + headers = [( + 'Set-Cookie', + self._generate_sid_cookie(sid, { + 'name': self.cookie, 'path': '/', 'SameSite': 'Lax' + }) + )] + try: + return self._ok(await s.poll(), headers=headers, + jsonp_index=jsonp_index) + except exceptions.QueueEmpty: + return self._bad_request() + + async def _trigger_event(self, event, *args, **kwargs): + """Invoke an event handler.""" + run_async = kwargs.pop('run_async', False) + ret = None + if event in self.handlers: + if asyncio.iscoroutinefunction(self.handlers[event]): + async def run_async_handler(): + try: + try: + return await self.handlers[event](*args) + except TypeError: + if event == 'disconnect' and \ + len(args) == 2: # pragma: no branch + # legacy disconnect events do not have a reason + # argument + return await self.handlers[event](args[0]) + else: # pragma: no cover + raise + except asyncio.CancelledError: # pragma: no cover + pass + except: + self.logger.exception(event + ' async handler error') + if event == 'connect': + # if connect handler raised error we reject the + # connection + return False + + if run_async: + ret = self.start_background_task(run_async_handler) + task_reference_holder.add(ret) + ret.add_done_callback(task_reference_holder.discard) + else: + ret = await run_async_handler() + else: + async def run_sync_handler(): + try: + try: + return self.handlers[event](*args) + except TypeError: + if event == 'disconnect' and \ + len(args) == 2: # pragma: no branch + # legacy disconnect events do not have a reason + # argument + return self.handlers[event](args[0]) + else: # pragma: no cover + raise + except: + self.logger.exception(event + ' handler error') + if event == 'connect': + # if connect handler raised error we reject the + # connection + return False + + if run_async: + ret = self.start_background_task(run_sync_handler) + task_reference_holder.add(ret) + ret.add_done_callback(task_reference_holder.discard) + else: + ret = await run_sync_handler() + return ret + + async def _service_task(self): # pragma: no cover + """Monitor connected clients and clean up those that time out.""" + loop = asyncio.get_running_loop() + self.service_task_event = self.create_event() + while not self.service_task_event.is_set(): + if len(self.sockets) == 0: + # nothing to do + try: + await asyncio.wait_for(self.service_task_event.wait(), + timeout=self.ping_timeout) + break + except asyncio.TimeoutError: + continue + + # go through the entire client list in a ping interval cycle + sleep_interval = self.ping_timeout / len(self.sockets) + + try: + # iterate over the current clients + for s in self.sockets.copy().values(): + if s.closed: + try: + del self.sockets[s.sid] + except KeyError: + # the socket could have also been removed by + # the _get_socket() method from another thread + pass + elif not s.closing: + await s.check_ping_timeout() + try: + await asyncio.wait_for(self.service_task_event.wait(), + timeout=sleep_interval) + raise KeyboardInterrupt() + except asyncio.TimeoutError: + continue + except ( + SystemExit, + KeyboardInterrupt, + asyncio.CancelledError, + GeneratorExit, + ): + self.logger.info('service task canceled') + break + except: + if loop.is_closed(): + self.logger.info('event loop is closed, exiting service ' + 'task') + break + + # an unexpected exception has occurred, log it and continue + self.logger.exception('service task exception') diff --git a/tapdown/lib/python3.11/site-packages/engineio/async_socket.py b/tapdown/lib/python3.11/site-packages/engineio/async_socket.py new file mode 100644 index 0000000..cfdbe1a --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/async_socket.py @@ -0,0 +1,261 @@ +import asyncio +import sys +import time + +from . import base_socket +from . import exceptions +from . import packet +from . import payload + + +class AsyncSocket(base_socket.BaseSocket): + async def poll(self): + """Wait for packets to send to the client.""" + try: + packets = [await asyncio.wait_for( + self.queue.get(), + self.server.ping_interval + self.server.ping_timeout)] + self.queue.task_done() + except (asyncio.TimeoutError, asyncio.CancelledError): + raise exceptions.QueueEmpty() + if packets == [None]: + return [] + while True: + try: + pkt = self.queue.get_nowait() + self.queue.task_done() + if pkt is None: + self.queue.put_nowait(None) + break + packets.append(pkt) + except asyncio.QueueEmpty: + break + return packets + + async def receive(self, pkt): + """Receive packet from the client.""" + self.server.logger.info('%s: Received packet %s data %s', + self.sid, packet.packet_names[pkt.packet_type], + pkt.data if not isinstance(pkt.data, bytes) + else '') + if pkt.packet_type == packet.PONG: + self.schedule_ping() + elif pkt.packet_type == packet.MESSAGE: + await self.server._trigger_event( + 'message', self.sid, pkt.data, + run_async=self.server.async_handlers) + elif pkt.packet_type == packet.UPGRADE: + await self.send(packet.Packet(packet.NOOP)) + elif pkt.packet_type == packet.CLOSE: + await self.close(wait=False, abort=True, + reason=self.server.reason.CLIENT_DISCONNECT) + else: + raise exceptions.UnknownPacketError() + + async def check_ping_timeout(self): + """Make sure the client is still sending pings.""" + if self.closed: + raise exceptions.SocketIsClosedError() + if self.last_ping and \ + time.time() - self.last_ping > self.server.ping_timeout: + self.server.logger.info('%s: Client is gone, closing socket', + self.sid) + # Passing abort=False here will cause close() to write a + # CLOSE packet. This has the effect of updating half-open sockets + # to their correct state of disconnected + await self.close(wait=False, abort=False, + reason=self.server.reason.PING_TIMEOUT) + return False + return True + + async def send(self, pkt): + """Send a packet to the client.""" + if not await self.check_ping_timeout(): + return + else: + await self.queue.put(pkt) + self.server.logger.info('%s: Sending packet %s data %s', + self.sid, packet.packet_names[pkt.packet_type], + pkt.data if not isinstance(pkt.data, bytes) + else '') + + async def handle_get_request(self, environ): + """Handle a long-polling GET request from the client.""" + connections = [ + s.strip() + for s in environ.get('HTTP_CONNECTION', '').lower().split(',')] + transport = environ.get('HTTP_UPGRADE', '').lower() + if 'upgrade' in connections and transport in self.upgrade_protocols: + self.server.logger.info('%s: Received request to upgrade to %s', + self.sid, transport) + return await getattr(self, '_upgrade_' + transport)(environ) + if self.upgrading or self.upgraded: + # we are upgrading to WebSocket, do not return any more packets + # through the polling endpoint + return [packet.Packet(packet.NOOP)] + try: + packets = await self.poll() + except exceptions.QueueEmpty: + exc = sys.exc_info() + await self.close(wait=False, + reason=self.server.reason.TRANSPORT_ERROR) + raise exc[1].with_traceback(exc[2]) + return packets + + async def handle_post_request(self, environ): + """Handle a long-polling POST request from the client.""" + length = int(environ.get('CONTENT_LENGTH', '0')) + if length > self.server.max_http_buffer_size: + raise exceptions.ContentTooLongError() + else: + body = (await environ['wsgi.input'].read(length)).decode('utf-8') + p = payload.Payload(encoded_payload=body) + for pkt in p.packets: + await self.receive(pkt) + + async def close(self, wait=True, abort=False, reason=None): + """Close the socket connection.""" + if not self.closed and not self.closing: + self.closing = True + await self.server._trigger_event( + 'disconnect', self.sid, + reason or self.server.reason.SERVER_DISCONNECT, + run_async=False) + if not abort: + await self.send(packet.Packet(packet.CLOSE)) + self.closed = True + if wait: + await self.queue.join() + + def schedule_ping(self): + self.server.start_background_task(self._send_ping) + + async def _send_ping(self): + self.last_ping = None + await asyncio.sleep(self.server.ping_interval) + if not self.closing and not self.closed: + self.last_ping = time.time() + await self.send(packet.Packet(packet.PING)) + + async def _upgrade_websocket(self, environ): + """Upgrade the connection from polling to websocket.""" + if self.upgraded: + raise OSError('Socket has been upgraded already') + if self.server._async['websocket'] is None: + # the selected async mode does not support websocket + return self.server._bad_request() + ws = self.server._async['websocket']( + self._websocket_handler, self.server) + return await ws(environ) + + async def _websocket_handler(self, ws): + """Engine.IO handler for websocket transport.""" + async def websocket_wait(): + data = await ws.wait() + if data and len(data) > self.server.max_http_buffer_size: + raise ValueError('packet is too large') + return data + + if self.connected: + # the socket was already connected, so this is an upgrade + self.upgrading = True # hold packet sends during the upgrade + + try: + pkt = await websocket_wait() + except OSError: # pragma: no cover + return + decoded_pkt = packet.Packet(encoded_packet=pkt) + if decoded_pkt.packet_type != packet.PING or \ + decoded_pkt.data != 'probe': + self.server.logger.info( + '%s: Failed websocket upgrade, no PING packet', self.sid) + self.upgrading = False + return + await ws.send(packet.Packet(packet.PONG, data='probe').encode()) + await self.queue.put(packet.Packet(packet.NOOP)) # end poll + + try: + pkt = await websocket_wait() + except OSError: # pragma: no cover + self.upgrading = False + return + decoded_pkt = packet.Packet(encoded_packet=pkt) + if decoded_pkt.packet_type != packet.UPGRADE: + self.upgraded = False + self.server.logger.info( + ('%s: Failed websocket upgrade, expected UPGRADE packet, ' + 'received %s instead.'), + self.sid, pkt) + self.upgrading = False + return + self.upgraded = True + self.upgrading = False + else: + self.connected = True + self.upgraded = True + + # start separate writer thread + async def writer(): + while True: + packets = None + try: + packets = await self.poll() + except exceptions.QueueEmpty: + break + if not packets: + # empty packet list returned -> connection closed + break + try: + for pkt in packets: + await ws.send(pkt.encode()) + except: + break + await ws.close() + + writer_task = asyncio.ensure_future(writer()) + + self.server.logger.info( + '%s: Upgrade to websocket successful', self.sid) + + while True: + p = None + wait_task = asyncio.ensure_future(websocket_wait()) + try: + p = await asyncio.wait_for( + wait_task, + self.server.ping_interval + self.server.ping_timeout) + except asyncio.CancelledError: # pragma: no cover + # there is a bug (https://bugs.python.org/issue30508) in + # asyncio that causes a "Task exception never retrieved" error + # to appear when wait_task raises an exception before it gets + # cancelled. Calling wait_task.exception() prevents the error + # from being issued in Python 3.6, but causes other errors in + # other versions, so we run it with all errors suppressed and + # hope for the best. + try: + wait_task.exception() + except: + pass + break + except: + break + if p is None: + # connection closed by client + break + pkt = packet.Packet(encoded_packet=p) + try: + await self.receive(pkt) + except exceptions.UnknownPacketError: # pragma: no cover + pass + except exceptions.SocketIsClosedError: # pragma: no cover + self.server.logger.info('Receive error -- socket is closed') + break + except: # pragma: no cover + # if we get an unexpected exception we log the error and exit + # the connection properly + self.server.logger.exception('Unknown receive error') + + await self.queue.put(None) # unlock the writer task so it can exit + await asyncio.wait_for(writer_task, timeout=None) + await self.close(wait=False, abort=True, + reason=self.server.reason.TRANSPORT_CLOSE) diff --git a/tapdown/lib/python3.11/site-packages/engineio/base_client.py b/tapdown/lib/python3.11/site-packages/engineio/base_client.py new file mode 100644 index 0000000..01a42c5 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/base_client.py @@ -0,0 +1,169 @@ +import logging +import signal +import threading +import time +import urllib +from . import packet + +default_logger = logging.getLogger('engineio.client') +connected_clients = [] + + +def signal_handler(sig, frame): + """SIGINT handler. + + Disconnect all active clients and then invoke the original signal handler. + """ + for client in connected_clients[:]: + if not client.is_asyncio_based(): + client.disconnect() + if callable(original_signal_handler): + return original_signal_handler(sig, frame) + else: # pragma: no cover + # Handle case where no original SIGINT handler was present. + return signal.default_int_handler(sig, frame) + + +original_signal_handler = None + + +class BaseClient: + event_names = ['connect', 'disconnect', 'message'] + + class reason: + """Disconnection reasons.""" + #: Client-initiated disconnection. + CLIENT_DISCONNECT = 'client disconnect' + #: Server-initiated disconnection. + SERVER_DISCONNECT = 'server disconnect' + #: Transport error. + TRANSPORT_ERROR = 'transport error' + + def __init__(self, logger=False, json=None, request_timeout=5, + http_session=None, ssl_verify=True, handle_sigint=True, + websocket_extra_options=None, timestamp_requests=True): + global original_signal_handler + if handle_sigint and original_signal_handler is None and \ + threading.current_thread() == threading.main_thread(): + original_signal_handler = signal.signal(signal.SIGINT, + signal_handler) + self.handlers = {} + self.base_url = None + self.transports = None + self.current_transport = None + self.sid = None + self.upgrades = None + self.ping_interval = None + self.ping_timeout = None + self.http = http_session + self.external_http = http_session is not None + self.handle_sigint = handle_sigint + self.ws = None + self.read_loop_task = None + self.write_loop_task = None + self.queue = self.create_queue() + self.queue_empty = self.get_queue_empty_exception() + self.state = 'disconnected' + self.ssl_verify = ssl_verify + self.websocket_extra_options = websocket_extra_options or {} + self.timestamp_requests = timestamp_requests + + if json is not None: + packet.Packet.json = json + if not isinstance(logger, bool): + self.logger = logger + else: + self.logger = default_logger + if self.logger.level == logging.NOTSET: + if logger: + self.logger.setLevel(logging.INFO) + else: + self.logger.setLevel(logging.ERROR) + self.logger.addHandler(logging.StreamHandler()) + + self.request_timeout = request_timeout + + def is_asyncio_based(self): + return False + + def on(self, event, handler=None): + """Register an event handler. + + :param event: The event name. Can be ``'connect'``, ``'message'`` or + ``'disconnect'``. + :param handler: The function that should be invoked to handle the + event. When this parameter is not given, the method + acts as a decorator for the handler function. + + Example usage:: + + # as a decorator: + @eio.on('connect') + def connect_handler(): + print('Connection request') + + # as a method: + def message_handler(msg): + print('Received message: ', msg) + eio.send('response') + eio.on('message', message_handler) + """ + if event not in self.event_names: + raise ValueError('Invalid event') + + def set_handler(handler): + self.handlers[event] = handler + return handler + + if handler is None: + return set_handler + set_handler(handler) + + def transport(self): + """Return the name of the transport currently in use. + + The possible values returned by this function are ``'polling'`` and + ``'websocket'``. + """ + return self.current_transport + + def _reset(self): + self.state = 'disconnected' + self.sid = None + + def _get_engineio_url(self, url, engineio_path, transport): + """Generate the Engine.IO connection URL.""" + engineio_path = engineio_path.strip('/') + parsed_url = urllib.parse.urlparse(url) + + if transport == 'polling': + scheme = 'http' + elif transport == 'websocket': + scheme = 'ws' + else: # pragma: no cover + raise ValueError('invalid transport') + if parsed_url.scheme in ['https', 'wss']: + scheme += 's' + + return ('{scheme}://{netloc}/{path}/?{query}' + '{sep}transport={transport}&EIO=4').format( + scheme=scheme, netloc=parsed_url.netloc, + path=engineio_path, query=parsed_url.query, + sep='&' if parsed_url.query else '', + transport=transport) + + def _get_url_timestamp(self): + """Generate the Engine.IO query string timestamp.""" + if not self.timestamp_requests: + return '' + return '&t=' + str(time.time()) + + def create_queue(self, *args, **kwargs): # pragma: no cover + """Create a queue object.""" + raise NotImplementedError('must be implemented in a subclass') + + def get_queue_empty_exception(self): # pragma: no cover + """Return the queue empty exception raised by queues created by the + ``create_queue()`` method. + """ + raise NotImplementedError('must be implemented in a subclass') diff --git a/tapdown/lib/python3.11/site-packages/engineio/base_server.py b/tapdown/lib/python3.11/site-packages/engineio/base_server.py new file mode 100644 index 0000000..7d717fb --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/base_server.py @@ -0,0 +1,358 @@ +import base64 +import gzip +import importlib +import io +import logging +import secrets +import zlib + +from . import packet +from . import payload + +default_logger = logging.getLogger('engineio.server') + + +class BaseServer: + compression_methods = ['gzip', 'deflate'] + event_names = ['connect', 'disconnect', 'message'] + valid_transports = ['polling', 'websocket'] + _default_monitor_clients = True + sequence_number = 0 + + class reason: + """Disconnection reasons.""" + #: Server-initiated disconnection. + SERVER_DISCONNECT = 'server disconnect' + #: Client-initiated disconnection. + CLIENT_DISCONNECT = 'client disconnect' + #: Ping timeout. + PING_TIMEOUT = 'ping timeout' + #: Transport close. + TRANSPORT_CLOSE = 'transport close' + #: Transport error. + TRANSPORT_ERROR = 'transport error' + + def __init__(self, async_mode=None, ping_interval=25, ping_timeout=20, + max_http_buffer_size=1000000, allow_upgrades=True, + http_compression=True, compression_threshold=1024, + cookie=None, cors_allowed_origins=None, + cors_credentials=True, logger=False, json=None, + async_handlers=True, monitor_clients=None, transports=None, + **kwargs): + self.ping_timeout = ping_timeout + if isinstance(ping_interval, tuple): + self.ping_interval = ping_interval[0] + self.ping_interval_grace_period = ping_interval[1] + else: + self.ping_interval = ping_interval + self.ping_interval_grace_period = 0 + self.max_http_buffer_size = max_http_buffer_size + self.allow_upgrades = allow_upgrades + self.http_compression = http_compression + self.compression_threshold = compression_threshold + self.cookie = cookie + self.cors_allowed_origins = cors_allowed_origins + self.cors_credentials = cors_credentials + self.async_handlers = async_handlers + self.sockets = {} + self.handlers = {} + self.log_message_keys = set() + self.start_service_task = monitor_clients \ + if monitor_clients is not None else self._default_monitor_clients + self.service_task_handle = None + self.service_task_event = None + if json is not None: + packet.Packet.json = json + if not isinstance(logger, bool): + self.logger = logger + else: + self.logger = default_logger + if self.logger.level == logging.NOTSET: + if logger: + self.logger.setLevel(logging.INFO) + else: + self.logger.setLevel(logging.ERROR) + self.logger.addHandler(logging.StreamHandler()) + modes = self.async_modes() + if async_mode is not None: + modes = [async_mode] if async_mode in modes else [] + self._async = None + self.async_mode = None + for mode in modes: + try: + self._async = importlib.import_module( + 'engineio.async_drivers.' + mode)._async + asyncio_based = self._async['asyncio'] \ + if 'asyncio' in self._async else False + if asyncio_based != self.is_asyncio_based(): + continue # pragma: no cover + self.async_mode = mode + break + except ImportError: + pass + if self.async_mode is None: + raise ValueError('Invalid async_mode specified') + if self.is_asyncio_based() and \ + ('asyncio' not in self._async or not + self._async['asyncio']): # pragma: no cover + raise ValueError('The selected async_mode is not asyncio ' + 'compatible') + if not self.is_asyncio_based() and 'asyncio' in self._async and \ + self._async['asyncio']: # pragma: no cover + raise ValueError('The selected async_mode requires asyncio and ' + 'must use the AsyncServer class') + if transports is not None: + if isinstance(transports, str): + transports = [transports] + transports = [transport for transport in transports + if transport in self.valid_transports] + if not transports: + raise ValueError('No valid transports provided') + self.transports = transports or self.valid_transports + self.logger.info('Server initialized for %s.', self.async_mode) + + def is_asyncio_based(self): + return False + + def async_modes(self): + return ['eventlet', 'gevent_uwsgi', 'gevent', 'threading'] + + def on(self, event, handler=None): + """Register an event handler. + + :param event: The event name. Can be ``'connect'``, ``'message'`` or + ``'disconnect'``. + :param handler: The function that should be invoked to handle the + event. When this parameter is not given, the method + acts as a decorator for the handler function. + + Example usage:: + + # as a decorator: + @eio.on('connect') + def connect_handler(sid, environ): + print('Connection request') + if environ['REMOTE_ADDR'] in blacklisted: + return False # reject + + # as a method: + def message_handler(sid, msg): + print('Received message: ', msg) + eio.send(sid, 'response') + eio.on('message', message_handler) + + The handler function receives the ``sid`` (session ID) for the + client as first argument. The ``'connect'`` event handler receives the + WSGI environment as a second argument, and can return ``False`` to + reject the connection. The ``'message'`` handler receives the message + payload as a second argument. The ``'disconnect'`` handler does not + take a second argument. + """ + if event not in self.event_names: + raise ValueError('Invalid event') + + def set_handler(handler): + self.handlers[event] = handler + return handler + + if handler is None: + return set_handler + set_handler(handler) + + def transport(self, sid): + """Return the name of the transport used by the client. + + The two possible values returned by this function are ``'polling'`` + and ``'websocket'``. + + :param sid: The session of the client. + """ + return 'websocket' if self._get_socket(sid).upgraded else 'polling' + + def create_queue(self, *args, **kwargs): + """Create a queue object using the appropriate async model. + + This is a utility function that applications can use to create a queue + without having to worry about using the correct call for the selected + async mode. + """ + return self._async['queue'](*args, **kwargs) + + def get_queue_empty_exception(self): + """Return the queue empty exception for the appropriate async model. + + This is a utility function that applications can use to work with a + queue without having to worry about using the correct call for the + selected async mode. + """ + return self._async['queue_empty'] + + def create_event(self, *args, **kwargs): + """Create an event object using the appropriate async model. + + This is a utility function that applications can use to create an + event without having to worry about using the correct call for the + selected async mode. + """ + return self._async['event'](*args, **kwargs) + + def generate_id(self): + """Generate a unique session id.""" + id = base64.b64encode( + secrets.token_bytes(12) + self.sequence_number.to_bytes(3, 'big')) + self.sequence_number = (self.sequence_number + 1) & 0xffffff + return id.decode('utf-8').replace('/', '_').replace('+', '-') + + def _generate_sid_cookie(self, sid, attributes): + """Generate the sid cookie.""" + cookie = attributes.get('name', 'io') + '=' + sid + for attribute, value in attributes.items(): + if attribute == 'name': + continue + if callable(value): + value = value() + if value is True: + cookie += '; ' + attribute + else: + cookie += '; ' + attribute + '=' + value + return cookie + + def _upgrades(self, sid, transport): + """Return the list of possible upgrades for a client connection.""" + if not self.allow_upgrades or self._get_socket(sid).upgraded or \ + transport == 'websocket': + return [] + if self._async['websocket'] is None: # pragma: no cover + self._log_error_once( + 'The WebSocket transport is not available, you must install a ' + 'WebSocket server that is compatible with your async mode to ' + 'enable it. See the documentation for details.', + 'no-websocket') + return [] + return ['websocket'] + + def _get_socket(self, sid): + """Return the socket object for a given session.""" + try: + s = self.sockets[sid] + except KeyError: + raise KeyError('Session not found') + if s.closed: + del self.sockets[sid] + raise KeyError('Session is disconnected') + return s + + def _ok(self, packets=None, headers=None, jsonp_index=None): + """Generate a successful HTTP response.""" + if packets is not None: + if headers is None: + headers = [] + headers += [('Content-Type', 'text/plain; charset=UTF-8')] + return {'status': '200 OK', + 'headers': headers, + 'response': payload.Payload(packets=packets).encode( + jsonp_index=jsonp_index).encode('utf-8')} + else: + return {'status': '200 OK', + 'headers': [('Content-Type', 'text/plain')], + 'response': b'OK'} + + def _bad_request(self, message=None): + """Generate a bad request HTTP error response.""" + if message is None: + message = 'Bad Request' + message = packet.Packet.json.dumps(message) + return {'status': '400 BAD REQUEST', + 'headers': [('Content-Type', 'text/plain')], + 'response': message.encode('utf-8')} + + def _method_not_found(self): + """Generate a method not found HTTP error response.""" + return {'status': '405 METHOD NOT FOUND', + 'headers': [('Content-Type', 'text/plain')], + 'response': b'Method Not Found'} + + def _unauthorized(self, message=None): + """Generate a unauthorized HTTP error response.""" + if message is None: + message = 'Unauthorized' + message = packet.Packet.json.dumps(message) + return {'status': '401 UNAUTHORIZED', + 'headers': [('Content-Type', 'application/json')], + 'response': message.encode('utf-8')} + + def _cors_allowed_origins(self, environ): + if self.cors_allowed_origins is None: + allowed_origins = [] + if 'wsgi.url_scheme' in environ and 'HTTP_HOST' in environ: + allowed_origins.append('{scheme}://{host}'.format( + scheme=environ['wsgi.url_scheme'], + host=environ['HTTP_HOST'])) + if 'HTTP_X_FORWARDED_PROTO' in environ or \ + 'HTTP_X_FORWARDED_HOST' in environ: + scheme = environ.get( + 'HTTP_X_FORWARDED_PROTO', + environ['wsgi.url_scheme']).split(',')[0].strip() + allowed_origins.append('{scheme}://{host}'.format( + scheme=scheme, host=environ.get( + 'HTTP_X_FORWARDED_HOST', + environ['HTTP_HOST']).split( + ',')[0].strip())) + elif self.cors_allowed_origins == '*': + allowed_origins = None + elif isinstance(self.cors_allowed_origins, str): + allowed_origins = [self.cors_allowed_origins] + elif callable(self.cors_allowed_origins): + origin = environ.get('HTTP_ORIGIN') + try: + is_allowed = self.cors_allowed_origins(origin, environ) + except TypeError: + is_allowed = self.cors_allowed_origins(origin) + allowed_origins = [origin] if is_allowed else [] + else: + if '*' in self.cors_allowed_origins: + allowed_origins = None + else: + allowed_origins = self.cors_allowed_origins + return allowed_origins + + def _cors_headers(self, environ): + """Return the cross-origin-resource-sharing headers.""" + if self.cors_allowed_origins == []: + # special case, CORS handling is completely disabled + return [] + headers = [] + allowed_origins = self._cors_allowed_origins(environ) + if 'HTTP_ORIGIN' in environ and \ + (allowed_origins is None or environ['HTTP_ORIGIN'] in + allowed_origins): + headers = [('Access-Control-Allow-Origin', environ['HTTP_ORIGIN'])] + if environ['REQUEST_METHOD'] == 'OPTIONS': + headers += [('Access-Control-Allow-Methods', 'OPTIONS, GET, POST')] + if 'HTTP_ACCESS_CONTROL_REQUEST_HEADERS' in environ: + headers += [('Access-Control-Allow-Headers', + environ['HTTP_ACCESS_CONTROL_REQUEST_HEADERS'])] + if self.cors_credentials: + headers += [('Access-Control-Allow-Credentials', 'true')] + return headers + + def _gzip(self, response): + """Apply gzip compression to a response.""" + bytesio = io.BytesIO() + with gzip.GzipFile(fileobj=bytesio, mode='w') as gz: + gz.write(response) + return bytesio.getvalue() + + def _deflate(self, response): + """Apply deflate compression to a response.""" + return zlib.compress(response) + + def _log_error_once(self, message, message_key): + """Log message with logging.ERROR level the first time, then log + with given level.""" + if message_key not in self.log_message_keys: + self.logger.error(message + ' (further occurrences of this error ' + 'will be logged with level INFO)') + self.log_message_keys.add(message_key) + else: + self.logger.info(message) diff --git a/tapdown/lib/python3.11/site-packages/engineio/base_socket.py b/tapdown/lib/python3.11/site-packages/engineio/base_socket.py new file mode 100644 index 0000000..6b5d7dc --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/base_socket.py @@ -0,0 +1,14 @@ +class BaseSocket: + upgrade_protocols = ['websocket'] + + def __init__(self, server, sid): + self.server = server + self.sid = sid + self.queue = self.server.create_queue() + self.last_ping = None + self.connected = False + self.upgrading = False + self.upgraded = False + self.closing = False + self.closed = False + self.session = {} diff --git a/tapdown/lib/python3.11/site-packages/engineio/client.py b/tapdown/lib/python3.11/site-packages/engineio/client.py new file mode 100644 index 0000000..c04e080 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/client.py @@ -0,0 +1,632 @@ +from base64 import b64encode +from engineio.json import JSONDecodeError +import logging +import queue +import ssl +import threading +import time +import urllib + +try: + import requests +except ImportError: # pragma: no cover + requests = None +try: + import websocket +except ImportError: # pragma: no cover + websocket = None +from . import base_client +from . import exceptions +from . import packet +from . import payload + +default_logger = logging.getLogger('engineio.client') + + +class Client(base_client.BaseClient): + """An Engine.IO client. + + This class implements a fully compliant Engine.IO web client with support + for websocket and long-polling transports. + + :param logger: To enable logging set to ``True`` or pass a logger object to + use. To disable logging set to ``False``. The default is + ``False``. Note that fatal errors are logged even when + ``logger`` is ``False``. + :param json: An alternative json module to use for encoding and decoding + packets. Custom json modules must have ``dumps`` and ``loads`` + functions that are compatible with the standard library + versions. + :param request_timeout: A timeout in seconds for requests. The default is + 5 seconds. + :param http_session: an initialized ``requests.Session`` object to be used + when sending requests to the server. Use it if you + need to add special client options such as proxy + servers, SSL certificates, custom CA bundle, etc. + :param ssl_verify: ``True`` to verify SSL certificates, or ``False`` to + skip SSL certificate verification, allowing + connections to servers with self signed certificates. + The default is ``True``. + :param handle_sigint: Set to ``True`` to automatically handle disconnection + when the process is interrupted, or to ``False`` to + leave interrupt handling to the calling application. + Interrupt handling can only be enabled when the + client instance is created in the main thread. + :param websocket_extra_options: Dictionary containing additional keyword + arguments passed to + ``websocket.create_connection()``. + :param timestamp_requests: If ``True`` a timestamp is added to the query + string of Socket.IO requests as a cache-busting + measure. Set to ``False`` to disable. + """ + def connect(self, url, headers=None, transports=None, + engineio_path='engine.io'): + """Connect to an Engine.IO server. + + :param url: The URL of the Engine.IO server. It can include custom + query string parameters if required by the server. + :param headers: A dictionary with custom headers to send with the + connection request. + :param transports: The list of allowed transports. Valid transports + are ``'polling'`` and ``'websocket'``. If not + given, the polling transport is connected first, + then an upgrade to websocket is attempted. + :param engineio_path: The endpoint where the Engine.IO server is + installed. The default value is appropriate for + most cases. + + Example usage:: + + eio = engineio.Client() + eio.connect('http://localhost:5000') + """ + if self.state != 'disconnected': + raise ValueError('Client is not in a disconnected state') + valid_transports = ['polling', 'websocket'] + if transports is not None: + if isinstance(transports, str): + transports = [transports] + transports = [transport for transport in transports + if transport in valid_transports] + if not transports: + raise ValueError('No valid transports provided') + self.transports = transports or valid_transports + return getattr(self, '_connect_' + self.transports[0])( + url, headers or {}, engineio_path) + + def wait(self): + """Wait until the connection with the server ends. + + Client applications can use this function to block the main thread + during the life of the connection. + """ + if self.read_loop_task: + self.read_loop_task.join() + + def send(self, data): + """Send a message to the server. + + :param data: The data to send to the server. Data can be of type + ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` + or ``dict``, the data will be serialized as JSON. + """ + self._send_packet(packet.Packet(packet.MESSAGE, data=data)) + + def disconnect(self, abort=False, reason=None): + """Disconnect from the server. + + :param abort: If set to ``True``, do not wait for background tasks + associated with the connection to end. + """ + if self.state == 'connected': + self._send_packet(packet.Packet(packet.CLOSE)) + self.queue.put(None) + self.state = 'disconnecting' + self._trigger_event('disconnect', + reason or self.reason.CLIENT_DISCONNECT, + run_async=False) + if self.current_transport == 'websocket': + self.ws.close() + if not abort: + self.read_loop_task.join() + self.state = 'disconnected' + try: + base_client.connected_clients.remove(self) + except ValueError: # pragma: no cover + pass + self._reset() + + def start_background_task(self, target, *args, **kwargs): + """Start a background task. + + This is a utility function that applications can use to start a + background task. + + :param target: the target function to execute. + :param args: arguments to pass to the function. + :param kwargs: keyword arguments to pass to the function. + + This function returns an object that represents the background task, + on which the ``join()`` method can be invoked to wait for the task to + complete. + """ + th = threading.Thread(target=target, args=args, kwargs=kwargs, + daemon=True) + th.start() + return th + + def sleep(self, seconds=0): + """Sleep for the requested amount of time.""" + return time.sleep(seconds) + + def create_queue(self, *args, **kwargs): + """Create a queue object.""" + return queue.Queue(*args, **kwargs) + + def get_queue_empty_exception(self): + """Return the queue empty exception raised by queues created by the + ``create_queue()`` method. + """ + return queue.Empty + + def create_event(self, *args, **kwargs): + """Create an event object.""" + return threading.Event(*args, **kwargs) + + def _reset(self): + super()._reset() + while True: # pragma: no cover + try: + self.queue.get_nowait() + self.queue.task_done() + except self.queue_empty: + break + + def _connect_polling(self, url, headers, engineio_path): + """Establish a long-polling connection to the Engine.IO server.""" + if requests is None: # pragma: no cover + # not installed + self.logger.error('requests package is not installed -- cannot ' + 'send HTTP requests!') + return + self.base_url = self._get_engineio_url(url, engineio_path, 'polling') + self.logger.info('Attempting polling connection to ' + self.base_url) + r = self._send_request( + 'GET', self.base_url + self._get_url_timestamp(), headers=headers, + timeout=self.request_timeout) + if r is None or isinstance(r, str): + self._reset() + raise exceptions.ConnectionError( + r or 'Connection refused by the server') + if r.status_code < 200 or r.status_code >= 300: + self._reset() + try: + arg = r.json() + except JSONDecodeError: + arg = None + raise exceptions.ConnectionError( + 'Unexpected status code {} in server response'.format( + r.status_code), arg) + try: + p = payload.Payload(encoded_payload=r.content.decode('utf-8')) + except ValueError: + raise exceptions.ConnectionError( + 'Unexpected response from server') from None + open_packet = p.packets[0] + if open_packet.packet_type != packet.OPEN: + raise exceptions.ConnectionError( + 'OPEN packet not returned by server') + self.logger.info( + 'Polling connection accepted with ' + str(open_packet.data)) + self.sid = open_packet.data['sid'] + self.upgrades = open_packet.data['upgrades'] + self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 + self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 + self.current_transport = 'polling' + self.base_url += '&sid=' + self.sid + + self.state = 'connected' + base_client.connected_clients.append(self) + self._trigger_event('connect', run_async=False) + + for pkt in p.packets[1:]: + self._receive_packet(pkt) + + if 'websocket' in self.upgrades and 'websocket' in self.transports: + # attempt to upgrade to websocket + if self._connect_websocket(url, headers, engineio_path): + # upgrade to websocket succeeded, we're done here + return + + # start background tasks associated with this client + self.write_loop_task = self.start_background_task(self._write_loop) + self.read_loop_task = self.start_background_task( + self._read_loop_polling) + + def _connect_websocket(self, url, headers, engineio_path): + """Establish or upgrade to a WebSocket connection with the server.""" + if websocket is None: # pragma: no cover + # not installed + self.logger.error('websocket-client package not installed, only ' + 'polling transport is available') + return False + websocket_url = self._get_engineio_url(url, engineio_path, 'websocket') + if self.sid: + self.logger.info( + 'Attempting WebSocket upgrade to ' + websocket_url) + upgrade = True + websocket_url += '&sid=' + self.sid + else: + upgrade = False + self.base_url = websocket_url + self.logger.info( + 'Attempting WebSocket connection to ' + websocket_url) + + # get cookies and other settings from the long-polling connection + # so that they are preserved when connecting to the WebSocket route + cookies = None + extra_options = {} + if self.http: + # cookies + cookies = '; '.join([f"{cookie.name}={cookie.value}" + for cookie in self.http.cookies]) + for header, value in headers.items(): + if header.lower() == 'cookie': + if cookies: + cookies += '; ' + cookies += value + del headers[header] + break + + # auth + if 'Authorization' not in headers and self.http.auth is not None: + if not isinstance(self.http.auth, tuple): # pragma: no cover + raise ValueError('Only basic authentication is supported') + basic_auth = '{}:{}'.format( + self.http.auth[0], self.http.auth[1]).encode('utf-8') + basic_auth = b64encode(basic_auth).decode('utf-8') + headers['Authorization'] = 'Basic ' + basic_auth + + # cert + # this can be given as ('certfile', 'keyfile') or just 'certfile' + if isinstance(self.http.cert, tuple): + extra_options['sslopt'] = { + 'certfile': self.http.cert[0], + 'keyfile': self.http.cert[1]} + elif self.http.cert: + extra_options['sslopt'] = {'certfile': self.http.cert} + + # proxies + if self.http.proxies: + proxy_url = None + if websocket_url.startswith('ws://'): + proxy_url = self.http.proxies.get( + 'ws', self.http.proxies.get('http')) + else: # wss:// + proxy_url = self.http.proxies.get( + 'wss', self.http.proxies.get('https')) + if proxy_url: + parsed_url = urllib.parse.urlparse( + proxy_url if '://' in proxy_url + else 'scheme://' + proxy_url) + extra_options['http_proxy_host'] = parsed_url.hostname + extra_options['http_proxy_port'] = parsed_url.port + extra_options['http_proxy_auth'] = ( + (parsed_url.username, parsed_url.password) + if parsed_url.username or parsed_url.password + else None) + + # verify + if isinstance(self.http.verify, str): + if 'sslopt' in extra_options: + extra_options['sslopt']['ca_certs'] = self.http.verify + else: + extra_options['sslopt'] = {'ca_certs': self.http.verify} + elif not self.http.verify: + self.ssl_verify = False + + if not self.ssl_verify: + if 'sslopt' in extra_options: + extra_options['sslopt'].update({"cert_reqs": ssl.CERT_NONE}) + else: + extra_options['sslopt'] = {"cert_reqs": ssl.CERT_NONE} + + # combine internally generated options with the ones supplied by the + # caller. The caller's options take precedence. + headers.update(self.websocket_extra_options.pop('header', {})) + extra_options['header'] = headers + extra_options['cookie'] = cookies + extra_options['enable_multithread'] = True + extra_options['timeout'] = self.request_timeout + extra_options.update(self.websocket_extra_options) + try: + ws = websocket.create_connection( + websocket_url + self._get_url_timestamp(), **extra_options) + except (ConnectionError, OSError, websocket.WebSocketException): + if upgrade: + self.logger.warning( + 'WebSocket upgrade failed: connection error') + return False + else: + raise exceptions.ConnectionError('Connection error') + if upgrade: + p = packet.Packet(packet.PING, data='probe').encode() + try: + ws.send(p) + except Exception as e: # pragma: no cover + self.logger.warning( + 'WebSocket upgrade failed: unexpected send exception: %s', + str(e)) + return False + try: + p = ws.recv() + except Exception as e: # pragma: no cover + self.logger.warning( + 'WebSocket upgrade failed: unexpected recv exception: %s', + str(e)) + return False + pkt = packet.Packet(encoded_packet=p) + if pkt.packet_type != packet.PONG or pkt.data != 'probe': + self.logger.warning( + 'WebSocket upgrade failed: no PONG packet') + return False + p = packet.Packet(packet.UPGRADE).encode() + try: + ws.send(p) + except Exception as e: # pragma: no cover + self.logger.warning( + 'WebSocket upgrade failed: unexpected send exception: %s', + str(e)) + return False + self.current_transport = 'websocket' + self.logger.info('WebSocket upgrade was successful') + else: + try: + p = ws.recv() + except Exception as e: # pragma: no cover + raise exceptions.ConnectionError( + 'Unexpected recv exception: ' + str(e)) + open_packet = packet.Packet(encoded_packet=p) + if open_packet.packet_type != packet.OPEN: + raise exceptions.ConnectionError('no OPEN packet') + self.logger.info( + 'WebSocket connection accepted with ' + str(open_packet.data)) + self.sid = open_packet.data['sid'] + self.upgrades = open_packet.data['upgrades'] + self.ping_interval = int(open_packet.data['pingInterval']) / 1000.0 + self.ping_timeout = int(open_packet.data['pingTimeout']) / 1000.0 + self.current_transport = 'websocket' + + self.state = 'connected' + base_client.connected_clients.append(self) + self._trigger_event('connect', run_async=False) + self.ws = ws + self.ws.settimeout(self.ping_interval + self.ping_timeout) + + # start background tasks associated with this client + self.write_loop_task = self.start_background_task(self._write_loop) + self.read_loop_task = self.start_background_task( + self._read_loop_websocket) + return True + + def _receive_packet(self, pkt): + """Handle incoming packets from the server.""" + packet_name = packet.packet_names[pkt.packet_type] \ + if pkt.packet_type < len(packet.packet_names) else 'UNKNOWN' + self.logger.info( + 'Received packet %s data %s', packet_name, + pkt.data if not isinstance(pkt.data, bytes) else '') + if pkt.packet_type == packet.MESSAGE: + self._trigger_event('message', pkt.data, run_async=True) + elif pkt.packet_type == packet.PING: + self._send_packet(packet.Packet(packet.PONG, pkt.data)) + elif pkt.packet_type == packet.CLOSE: + self.disconnect(abort=True, reason=self.reason.SERVER_DISCONNECT) + elif pkt.packet_type == packet.NOOP: + pass + else: + self.logger.error('Received unexpected packet of type %s', + pkt.packet_type) + + def _send_packet(self, pkt): + """Queue a packet to be sent to the server.""" + if self.state != 'connected': + return + self.queue.put(pkt) + self.logger.info( + 'Sending packet %s data %s', + packet.packet_names[pkt.packet_type], + pkt.data if not isinstance(pkt.data, bytes) else '') + + def _send_request( + self, method, url, headers=None, body=None, + timeout=None): # pragma: no cover + if self.http is None: + self.http = requests.Session() + if not self.ssl_verify: + self.http.verify = False + try: + return self.http.request(method, url, headers=headers, data=body, + timeout=timeout) + except requests.exceptions.RequestException as exc: + self.logger.info('HTTP %s request to %s failed with error %s.', + method, url, exc) + return str(exc) + + def _trigger_event(self, event, *args, **kwargs): + """Invoke an event handler.""" + run_async = kwargs.pop('run_async', False) + if event in self.handlers: + if run_async: + return self.start_background_task(self.handlers[event], *args) + else: + try: + try: + return self.handlers[event](*args) + except TypeError: + if event == 'disconnect' and \ + len(args) == 1: # pragma: no branch + # legacy disconnect events do not have a reason + # argument + return self.handlers[event]() + else: # pragma: no cover + raise + except: + self.logger.exception(event + ' handler error') + + def _read_loop_polling(self): + """Read packets by polling the Engine.IO server.""" + while self.state == 'connected' and self.write_loop_task: + self.logger.info( + 'Sending polling GET request to ' + self.base_url) + r = self._send_request( + 'GET', self.base_url + self._get_url_timestamp(), + timeout=max(self.ping_interval, self.ping_timeout) + 5) + if r is None or isinstance(r, str): + self.logger.warning( + r or 'Connection refused by the server, aborting') + self.queue.put(None) + break + if r.status_code < 200 or r.status_code >= 300: + self.logger.warning('Unexpected status code %s in server ' + 'response, aborting', r.status_code) + self.queue.put(None) + break + try: + p = payload.Payload(encoded_payload=r.content.decode('utf-8')) + except ValueError: + self.logger.warning( + 'Unexpected packet from server, aborting') + self.queue.put(None) + break + for pkt in p.packets: + self._receive_packet(pkt) + + if self.write_loop_task: # pragma: no branch + self.logger.info('Waiting for write loop task to end') + self.write_loop_task.join() + if self.state == 'connected': + self._trigger_event('disconnect', self.reason.TRANSPORT_ERROR, + run_async=False) + try: + base_client.connected_clients.remove(self) + except ValueError: # pragma: no cover + pass + self._reset() + self.logger.info('Exiting read loop task') + + def _read_loop_websocket(self): + """Read packets from the Engine.IO WebSocket connection.""" + while self.state == 'connected': + p = None + try: + p = self.ws.recv() + if len(p) == 0 and not self.ws.connected: # pragma: no cover + # websocket client can return an empty string after close + raise websocket.WebSocketConnectionClosedException() + except websocket.WebSocketTimeoutException: + self.logger.warning( + 'Server has stopped communicating, aborting') + self.queue.put(None) + break + except websocket.WebSocketConnectionClosedException: + self.logger.warning( + 'WebSocket connection was closed, aborting') + self.queue.put(None) + break + except Exception as e: # pragma: no cover + if type(e) is OSError and e.errno == 9: + self.logger.info( + 'WebSocket connection is closing, aborting') + else: + self.logger.info( + 'Unexpected error receiving packet: "%s", aborting', + str(e)) + self.queue.put(None) + break + try: + pkt = packet.Packet(encoded_packet=p) + except Exception as e: # pragma: no cover + self.logger.info( + 'Unexpected error decoding packet: "%s", aborting', str(e)) + self.queue.put(None) + break + self._receive_packet(pkt) + + if self.write_loop_task: # pragma: no branch + self.logger.info('Waiting for write loop task to end') + self.write_loop_task.join() + if self.state == 'connected': + self._trigger_event('disconnect', self.reason.TRANSPORT_ERROR, + run_async=False) + try: + base_client.connected_clients.remove(self) + except ValueError: # pragma: no cover + pass + self._reset() + self.logger.info('Exiting read loop task') + + def _write_loop(self): + """This background task sends packages to the server as they are + pushed to the send queue. + """ + while self.state == 'connected': + # to simplify the timeout handling, use the maximum of the + # ping interval and ping timeout as timeout, with an extra 5 + # seconds grace period + timeout = max(self.ping_interval, self.ping_timeout) + 5 + packets = None + try: + packets = [self.queue.get(timeout=timeout)] + except self.queue_empty: + self.logger.error('packet queue is empty, aborting') + break + if packets == [None]: + self.queue.task_done() + packets = [] + else: + while True: + try: + packets.append(self.queue.get(block=False)) + except self.queue_empty: + break + if packets[-1] is None: + packets = packets[:-1] + self.queue.task_done() + break + if not packets: + # empty packet list returned -> connection closed + break + if self.current_transport == 'polling': + p = payload.Payload(packets=packets) + r = self._send_request( + 'POST', self.base_url, body=p.encode(), + headers={'Content-Type': 'text/plain'}, + timeout=self.request_timeout) + for pkt in packets: + self.queue.task_done() + if r is None or isinstance(r, str): + self.logger.warning( + r or 'Connection refused by the server, aborting') + break + if r.status_code < 200 or r.status_code >= 300: + self.logger.warning('Unexpected status code %s in server ' + 'response, aborting', r.status_code) + self.write_loop_task = None + break + else: + # websocket + try: + for pkt in packets: + encoded_packet = pkt.encode() + if pkt.binary: + self.ws.send_binary(encoded_packet) + else: + self.ws.send(encoded_packet) + self.queue.task_done() + except (websocket.WebSocketConnectionClosedException, + BrokenPipeError, OSError): + self.logger.warning( + 'WebSocket connection was closed, aborting') + break + self.logger.info('Exiting write loop task') diff --git a/tapdown/lib/python3.11/site-packages/engineio/exceptions.py b/tapdown/lib/python3.11/site-packages/engineio/exceptions.py new file mode 100644 index 0000000..fb0b3e0 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/exceptions.py @@ -0,0 +1,22 @@ +class EngineIOError(Exception): + pass + + +class ContentTooLongError(EngineIOError): + pass + + +class UnknownPacketError(EngineIOError): + pass + + +class QueueEmpty(EngineIOError): + pass + + +class SocketIsClosedError(EngineIOError): + pass + + +class ConnectionError(EngineIOError): + pass diff --git a/tapdown/lib/python3.11/site-packages/engineio/json.py b/tapdown/lib/python3.11/site-packages/engineio/json.py new file mode 100644 index 0000000..b612556 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/json.py @@ -0,0 +1,16 @@ +"""JSON-compatible module with sane defaults.""" + +from json import * # noqa: F401, F403 +from json import loads as original_loads + + +def _safe_int(s): + if len(s) > 100: + raise ValueError('Integer is too large') + return int(s) + + +def loads(*args, **kwargs): + if 'parse_int' not in kwargs: # pragma: no cover + kwargs['parse_int'] = _safe_int + return original_loads(*args, **kwargs) diff --git a/tapdown/lib/python3.11/site-packages/engineio/middleware.py b/tapdown/lib/python3.11/site-packages/engineio/middleware.py new file mode 100644 index 0000000..0e34fb0 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/middleware.py @@ -0,0 +1,86 @@ +import os +from engineio.static_files import get_static_file + + +class WSGIApp: + """WSGI application middleware for Engine.IO. + + This middleware dispatches traffic to an Engine.IO application. It can + also serve a list of static files to the client, or forward unrelated + HTTP traffic to another WSGI application. + + :param engineio_app: The Engine.IO server. Must be an instance of the + ``engineio.Server`` class. + :param wsgi_app: The WSGI app that receives all other traffic. + :param static_files: A dictionary with static file mapping rules. See the + documentation for details on this argument. + :param engineio_path: The endpoint where the Engine.IO application should + be installed. The default value is appropriate for + most cases. + + Example usage:: + + import engineio + import eventlet + + eio = engineio.Server() + app = engineio.WSGIApp(eio, static_files={ + '/': {'content_type': 'text/html', 'filename': 'index.html'}, + '/index.html': {'content_type': 'text/html', + 'filename': 'index.html'}, + }) + eventlet.wsgi.server(eventlet.listen(('', 8000)), app) + """ + def __init__(self, engineio_app, wsgi_app=None, static_files=None, + engineio_path='engine.io'): + self.engineio_app = engineio_app + self.wsgi_app = wsgi_app + self.engineio_path = engineio_path + if not self.engineio_path.startswith('/'): + self.engineio_path = '/' + self.engineio_path + if not self.engineio_path.endswith('/'): + self.engineio_path += '/' + self.static_files = static_files or {} + + def __call__(self, environ, start_response): + if 'gunicorn.socket' in environ: + # gunicorn saves the socket under environ['gunicorn.socket'], while + # eventlet saves it under environ['eventlet.input']. Eventlet also + # stores the socket inside a wrapper class, while gunicon writes it + # directly into the environment. To give eventlet's WebSocket + # module access to this socket when running under gunicorn, here we + # copy the socket to the eventlet format. + class Input: + def __init__(self, socket): + self.socket = socket + + def get_socket(self): + return self.socket + + environ['eventlet.input'] = Input(environ['gunicorn.socket']) + path = environ['PATH_INFO'] + if path is not None and path.startswith(self.engineio_path): + return self.engineio_app.handle_request(environ, start_response) + else: + static_file = get_static_file(path, self.static_files) \ + if self.static_files else None + if static_file and os.path.exists(static_file['filename']): + start_response( + '200 OK', + [('Content-Type', static_file['content_type'])]) + with open(static_file['filename'], 'rb') as f: + return [f.read()] + elif self.wsgi_app is not None: + return self.wsgi_app(environ, start_response) + return self.not_found(start_response) + + def not_found(self, start_response): + start_response("404 Not Found", [('Content-Type', 'text/plain')]) + return [b'Not Found'] + + +class Middleware(WSGIApp): + """This class has been renamed to ``WSGIApp`` and is now deprecated.""" + def __init__(self, engineio_app, wsgi_app=None, + engineio_path='engine.io'): + super().__init__(engineio_app, wsgi_app, engineio_path=engineio_path) diff --git a/tapdown/lib/python3.11/site-packages/engineio/packet.py b/tapdown/lib/python3.11/site-packages/engineio/packet.py new file mode 100644 index 0000000..40bb6df --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/packet.py @@ -0,0 +1,82 @@ +import base64 +from engineio import json as _json + +(OPEN, CLOSE, PING, PONG, MESSAGE, UPGRADE, NOOP) = (0, 1, 2, 3, 4, 5, 6) +packet_names = ['OPEN', 'CLOSE', 'PING', 'PONG', 'MESSAGE', 'UPGRADE', 'NOOP'] + +binary_types = (bytes, bytearray) + + +class Packet: + """Engine.IO packet.""" + + json = _json + + def __init__(self, packet_type=NOOP, data=None, encoded_packet=None): + self.packet_type = packet_type + self.data = data + self.encode_cache = None + if isinstance(data, str): + self.binary = False + elif isinstance(data, binary_types): + self.binary = True + else: + self.binary = False + if self.binary and self.packet_type != MESSAGE: + raise ValueError('Binary packets can only be of type MESSAGE') + if encoded_packet is not None: + self.decode(encoded_packet) + + def encode(self, b64=False): + """Encode the packet for transmission. + + Note: as a performance optimization, subsequent calls to this method + will return a cached encoded packet, even if the data has changed. + """ + if self.encode_cache: + return self.encode_cache + if self.binary: + if b64: + encoded_packet = 'b' + base64.b64encode(self.data).decode( + 'utf-8') + else: + encoded_packet = self.data + else: + encoded_packet = str(self.packet_type) + if isinstance(self.data, str): + encoded_packet += self.data + elif isinstance(self.data, dict) or isinstance(self.data, list): + encoded_packet += self.json.dumps(self.data, + separators=(',', ':')) + elif self.data is not None: + encoded_packet += str(self.data) + self.encode_cache = encoded_packet + return encoded_packet + + def decode(self, encoded_packet): + """Decode a transmitted package.""" + self.binary = isinstance(encoded_packet, binary_types) + if not self.binary and len(encoded_packet) == 0: + raise ValueError('Invalid empty packet received') + b64 = not self.binary and encoded_packet[0] == 'b' + if b64: + self.binary = True + self.packet_type = MESSAGE + self.data = base64.b64decode(encoded_packet[1:]) + else: + if self.binary and not isinstance(encoded_packet, bytes): + encoded_packet = bytes(encoded_packet) + if self.binary: + self.packet_type = MESSAGE + self.data = encoded_packet + else: + self.packet_type = int(encoded_packet[0]) + try: + if encoded_packet[1].isnumeric(): + # do not allow integer payloads, see + # github.com/miguelgrinberg/python-engineio/issues/75 + # for background on this decision + raise ValueError + self.data = self.json.loads(encoded_packet[1:]) + except (ValueError, IndexError): + self.data = encoded_packet[1:] diff --git a/tapdown/lib/python3.11/site-packages/engineio/payload.py b/tapdown/lib/python3.11/site-packages/engineio/payload.py new file mode 100644 index 0000000..775241b --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/payload.py @@ -0,0 +1,46 @@ +import urllib + +from . import packet + + +class Payload: + """Engine.IO payload.""" + max_decode_packets = 16 + + def __init__(self, packets=None, encoded_payload=None): + self.packets = packets or [] + if encoded_payload is not None: + self.decode(encoded_payload) + + def encode(self, jsonp_index=None): + """Encode the payload for transmission.""" + encoded_payload = '' + for pkt in self.packets: + if encoded_payload: + encoded_payload += '\x1e' + encoded_payload += pkt.encode(b64=True) + if jsonp_index is not None: + encoded_payload = '___eio[' + \ + str(jsonp_index) + \ + ']("' + \ + encoded_payload.replace('"', '\\"') + \ + '");' + return encoded_payload + + def decode(self, encoded_payload): + """Decode a transmitted payload.""" + self.packets = [] + + if len(encoded_payload) == 0: + return + + # JSONP POST payload starts with 'd=' + if encoded_payload.startswith('d='): + encoded_payload = urllib.parse.parse_qs( + encoded_payload)['d'][0] + + encoded_packets = encoded_payload.split('\x1e') + if len(encoded_packets) > self.max_decode_packets: + raise ValueError('Too many packets in payload') + self.packets = [packet.Packet(encoded_packet=encoded_packet) + for encoded_packet in encoded_packets] diff --git a/tapdown/lib/python3.11/site-packages/engineio/server.py b/tapdown/lib/python3.11/site-packages/engineio/server.py new file mode 100644 index 0000000..59f690c --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/server.py @@ -0,0 +1,503 @@ +import logging +import urllib + +from . import base_server +from . import exceptions +from . import packet +from . import socket + +default_logger = logging.getLogger('engineio.server') + + +class Server(base_server.BaseServer): + """An Engine.IO server. + + This class implements a fully compliant Engine.IO web server with support + for websocket and long-polling transports. + + :param async_mode: The asynchronous model to use. See the Deployment + section in the documentation for a description of the + available options. Valid async modes are "threading", + "eventlet", "gevent" and "gevent_uwsgi". If this + argument is not given, "eventlet" is tried first, then + "gevent_uwsgi", then "gevent", and finally "threading". + The first async mode that has all its dependencies + installed is the one that is chosen. + :param ping_interval: The interval in seconds at which the server pings + the client. The default is 25 seconds. For advanced + control, a two element tuple can be given, where + the first number is the ping interval and the second + is a grace period added by the server. + :param ping_timeout: The time in seconds that the client waits for the + server to respond before disconnecting. The default + is 20 seconds. + :param max_http_buffer_size: The maximum size that is accepted for incoming + messages. The default is 1,000,000 bytes. In + spite of its name, the value set in this + argument is enforced for HTTP long-polling and + WebSocket connections. + :param allow_upgrades: Whether to allow transport upgrades or not. The + default is ``True``. + :param http_compression: Whether to compress packages when using the + polling transport. The default is ``True``. + :param compression_threshold: Only compress messages when their byte size + is greater than this value. The default is + 1024 bytes. + :param cookie: If set to a string, it is the name of the HTTP cookie the + server sends back tot he client containing the client + session id. If set to a dictionary, the ``'name'`` key + contains the cookie name and other keys define cookie + attributes, where the value of each attribute can be a + string, a callable with no arguments, or a boolean. If set + to ``None`` (the default), a cookie is not sent to the + client. + :param cors_allowed_origins: Origin or list of origins that are allowed to + connect to this server. Only the same origin + is allowed by default. Set this argument to + ``'*'`` or ``['*']`` to allow all origins, or + to ``[]`` to disable CORS handling. + :param cors_credentials: Whether credentials (cookies, authentication) are + allowed in requests to this server. The default + is ``True``. + :param logger: To enable logging set to ``True`` or pass a logger object to + use. To disable logging set to ``False``. The default is + ``False``. Note that fatal errors are logged even when + ``logger`` is ``False``. + :param json: An alternative json module to use for encoding and decoding + packets. Custom json modules must have ``dumps`` and ``loads`` + functions that are compatible with the standard library + versions. + :param async_handlers: If set to ``True``, run message event handlers in + non-blocking threads. To run handlers synchronously, + set to ``False``. The default is ``True``. + :param monitor_clients: If set to ``True``, a background task will ensure + inactive clients are closed. Set to ``False`` to + disable the monitoring task (not recommended). The + default is ``True``. + :param transports: The list of allowed transports. Valid transports + are ``'polling'`` and ``'websocket'``. Defaults to + ``['polling', 'websocket']``. + :param kwargs: Reserved for future extensions, any additional parameters + given as keyword arguments will be silently ignored. + """ + def send(self, sid, data): + """Send a message to a client. + + :param sid: The session id of the recipient client. + :param data: The data to send to the client. Data can be of type + ``str``, ``bytes``, ``list`` or ``dict``. If a ``list`` + or ``dict``, the data will be serialized as JSON. + """ + self.send_packet(sid, packet.Packet(packet.MESSAGE, data=data)) + + def send_packet(self, sid, pkt): + """Send a raw packet to a client. + + :param sid: The session id of the recipient client. + :param pkt: The packet to send to the client. + """ + try: + socket = self._get_socket(sid) + except KeyError: + # the socket is not available + self.logger.warning('Cannot send to sid %s', sid) + return + socket.send(pkt) + + def get_session(self, sid): + """Return the user session for a client. + + :param sid: The session id of the client. + + The return value is a dictionary. Modifications made to this + dictionary are not guaranteed to be preserved unless + ``save_session()`` is called, or when the ``session`` context manager + is used. + """ + socket = self._get_socket(sid) + return socket.session + + def save_session(self, sid, session): + """Store the user session for a client. + + :param sid: The session id of the client. + :param session: The session dictionary. + """ + socket = self._get_socket(sid) + socket.session = session + + def session(self, sid): + """Return the user session for a client with context manager syntax. + + :param sid: The session id of the client. + + This is a context manager that returns the user session dictionary for + the client. Any changes that are made to this dictionary inside the + context manager block are saved back to the session. Example usage:: + + @eio.on('connect') + def on_connect(sid, environ): + username = authenticate_user(environ) + if not username: + return False + with eio.session(sid) as session: + session['username'] = username + + @eio.on('message') + def on_message(sid, msg): + with eio.session(sid) as session: + print('received message from ', session['username']) + """ + class _session_context_manager: + def __init__(self, server, sid): + self.server = server + self.sid = sid + self.session = None + + def __enter__(self): + self.session = self.server.get_session(sid) + return self.session + + def __exit__(self, *args): + self.server.save_session(sid, self.session) + + return _session_context_manager(self, sid) + + def disconnect(self, sid=None): + """Disconnect a client. + + :param sid: The session id of the client to close. If this parameter + is not given, then all clients are closed. + """ + if sid is not None: + try: + socket = self._get_socket(sid) + except KeyError: # pragma: no cover + # the socket was already closed or gone + pass + else: + socket.close(reason=self.reason.SERVER_DISCONNECT) + if sid in self.sockets: # pragma: no cover + del self.sockets[sid] + else: + for client in self.sockets.copy().values(): + client.close(reason=self.reason.SERVER_DISCONNECT) + self.sockets = {} + + def handle_request(self, environ, start_response): + """Handle an HTTP request from the client. + + This is the entry point of the Engine.IO application, using the same + interface as a WSGI application. For the typical usage, this function + is invoked by the :class:`Middleware` instance, but it can be invoked + directly when the middleware is not used. + + :param environ: The WSGI environment. + :param start_response: The WSGI ``start_response`` function. + + This function returns the HTTP response body to deliver to the client + as a byte sequence. + """ + if self.cors_allowed_origins != []: + # Validate the origin header if present + # This is important for WebSocket more than for HTTP, since + # browsers only apply CORS controls to HTTP. + origin = environ.get('HTTP_ORIGIN') + if origin: + allowed_origins = self._cors_allowed_origins(environ) + if allowed_origins is not None and origin not in \ + allowed_origins: + self._log_error_once( + origin + ' is not an accepted origin.', 'bad-origin') + r = self._bad_request('Not an accepted origin.') + start_response(r['status'], r['headers']) + return [r['response']] + + method = environ['REQUEST_METHOD'] + query = urllib.parse.parse_qs(environ.get('QUERY_STRING', '')) + jsonp = False + jsonp_index = None + + # make sure the client uses an allowed transport + transport = query.get('transport', ['polling'])[0] + if transport not in self.transports: + self._log_error_once('Invalid transport', 'bad-transport') + r = self._bad_request('Invalid transport') + start_response(r['status'], r['headers']) + return [r['response']] + + # make sure the client speaks a compatible Engine.IO version + sid = query['sid'][0] if 'sid' in query else None + if sid is None and query.get('EIO') != ['4']: + self._log_error_once( + 'The client is using an unsupported version of the Socket.IO ' + 'or Engine.IO protocols', 'bad-version') + r = self._bad_request( + 'The client is using an unsupported version of the Socket.IO ' + 'or Engine.IO protocols') + start_response(r['status'], r['headers']) + return [r['response']] + + if 'j' in query: + jsonp = True + try: + jsonp_index = int(query['j'][0]) + except (ValueError, KeyError, IndexError): + # Invalid JSONP index number + pass + + if jsonp and jsonp_index is None: + self._log_error_once('Invalid JSONP index number', + 'bad-jsonp-index') + r = self._bad_request('Invalid JSONP index number') + elif method == 'GET': + upgrade_header = environ.get('HTTP_UPGRADE').lower() \ + if 'HTTP_UPGRADE' in environ else None + if sid is None: + # transport must be one of 'polling' or 'websocket'. + # if 'websocket', the HTTP_UPGRADE header must match. + if transport == 'polling' \ + or transport == upgrade_header == 'websocket': + r = self._handle_connect(environ, start_response, + transport, jsonp_index) + else: + self._log_error_once('Invalid websocket upgrade', + 'bad-upgrade') + r = self._bad_request('Invalid websocket upgrade') + else: + if sid not in self.sockets: + self._log_error_once(f'Invalid session {sid}', 'bad-sid') + r = self._bad_request(f'Invalid session {sid}') + else: + try: + socket = self._get_socket(sid) + except KeyError as e: # pragma: no cover + self._log_error_once(f'{e} {sid}', 'bad-sid') + r = self._bad_request(f'{e} {sid}') + else: + if self.transport(sid) != transport and \ + transport != upgrade_header: + self._log_error_once( + f'Invalid transport for session {sid}', + 'bad-transport') + r = self._bad_request('Invalid transport') + else: + try: + packets = socket.handle_get_request( + environ, start_response) + if isinstance(packets, list): + r = self._ok(packets, + jsonp_index=jsonp_index) + else: + r = packets + except exceptions.EngineIOError: + if sid in self.sockets: # pragma: no cover + self.disconnect(sid) + r = self._bad_request() + if sid in self.sockets and \ + self.sockets[sid].closed: + del self.sockets[sid] + elif method == 'POST': + if sid is None or sid not in self.sockets: + self._log_error_once(f'Invalid session {sid}', 'bad-sid') + r = self._bad_request(f'Invalid session {sid}') + else: + socket = self._get_socket(sid) + try: + socket.handle_post_request(environ) + r = self._ok(jsonp_index=jsonp_index) + except exceptions.EngineIOError: + if sid in self.sockets: # pragma: no cover + self.disconnect(sid) + r = self._bad_request() + except: # pragma: no cover + # for any other unexpected errors, we log the error + # and keep going + self.logger.exception('post request handler error') + r = self._ok(jsonp_index=jsonp_index) + elif method == 'OPTIONS': + r = self._ok() + else: + self.logger.warning('Method %s not supported', method) + r = self._method_not_found() + + if not isinstance(r, dict): + return r + if self.http_compression and \ + len(r['response']) >= self.compression_threshold: + encodings = [e.split(';')[0].strip() for e in + environ.get('HTTP_ACCEPT_ENCODING', '').split(',')] + for encoding in encodings: + if encoding in self.compression_methods: + r['response'] = \ + getattr(self, '_' + encoding)(r['response']) + r['headers'] += [('Content-Encoding', encoding)] + break + cors_headers = self._cors_headers(environ) + start_response(r['status'], r['headers'] + cors_headers) + return [r['response']] + + def shutdown(self): + """Stop Socket.IO background tasks. + + This method stops background activity initiated by the Socket.IO + server. It must be called before shutting down the web server. + """ + self.logger.info('Socket.IO is shutting down') + if self.service_task_event: # pragma: no cover + self.service_task_event.set() + self.service_task_handle.join() + self.service_task_handle = None + + def start_background_task(self, target, *args, **kwargs): + """Start a background task using the appropriate async model. + + This is a utility function that applications can use to start a + background task using the method that is compatible with the + selected async mode. + + :param target: the target function to execute. + :param args: arguments to pass to the function. + :param kwargs: keyword arguments to pass to the function. + + This function returns an object that represents the background task, + on which the ``join()`` methond can be invoked to wait for the task to + complete. + """ + th = self._async['thread'](target=target, args=args, kwargs=kwargs) + th.start() + return th # pragma: no cover + + def sleep(self, seconds=0): + """Sleep for the requested amount of time using the appropriate async + model. + + This is a utility function that applications can use to put a task to + sleep without having to worry about using the correct call for the + selected async mode. + """ + return self._async['sleep'](seconds) + + def _handle_connect(self, environ, start_response, transport, + jsonp_index=None): + """Handle a client connection request.""" + if self.start_service_task: + # start the service task to monitor connected clients + self.start_service_task = False + self.service_task_handle = self.start_background_task( + self._service_task) + + sid = self.generate_id() + s = socket.Socket(self, sid) + self.sockets[sid] = s + + pkt = packet.Packet(packet.OPEN, { + 'sid': sid, + 'upgrades': self._upgrades(sid, transport), + 'pingTimeout': int(self.ping_timeout * 1000), + 'pingInterval': int( + self.ping_interval + self.ping_interval_grace_period) * 1000, + 'maxPayload': self.max_http_buffer_size, + }) + s.send(pkt) + s.schedule_ping() + + # NOTE: some sections below are marked as "no cover" to workaround + # what seems to be a bug in the coverage package. All the lines below + # are covered by tests, but some are not reported as such for some + # reason + ret = self._trigger_event('connect', sid, environ, run_async=False) + if ret is not None and ret is not True: # pragma: no cover + del self.sockets[sid] + self.logger.warning('Application rejected connection') + return self._unauthorized(ret or None) + + if transport == 'websocket': # pragma: no cover + ret = s.handle_get_request(environ, start_response) + if s.closed and sid in self.sockets: + # websocket connection ended, so we are done + del self.sockets[sid] + return ret + else: # pragma: no cover + s.connected = True + headers = None + if self.cookie: + if isinstance(self.cookie, dict): + headers = [( + 'Set-Cookie', + self._generate_sid_cookie(sid, self.cookie) + )] + else: + headers = [( + 'Set-Cookie', + self._generate_sid_cookie(sid, { + 'name': self.cookie, 'path': '/', 'SameSite': 'Lax' + }) + )] + try: + return self._ok(s.poll(), headers=headers, + jsonp_index=jsonp_index) + except exceptions.QueueEmpty: + return self._bad_request() + + def _trigger_event(self, event, *args, **kwargs): + """Invoke an event handler.""" + run_async = kwargs.pop('run_async', False) + if event in self.handlers: + def run_handler(): + try: + try: + return self.handlers[event](*args) + except TypeError: + if event == 'disconnect' and \ + len(args) == 2: # pragma: no branch + # legacy disconnect events do not have a reason + # argument + return self.handlers[event](args[0]) + else: # pragma: no cover + raise + except: + self.logger.exception(event + ' handler error') + if event == 'connect': + # if connect handler raised error we reject the + # connection + return False + + if run_async: + return self.start_background_task(run_handler) + else: + return run_handler() + + def _service_task(self): # pragma: no cover + """Monitor connected clients and clean up those that time out.""" + self.service_task_event = self.create_event() + while not self.service_task_event.is_set(): + if len(self.sockets) == 0: + # nothing to do + if self.service_task_event.wait(timeout=self.ping_timeout): + break + continue + + # go through the entire client list in a ping interval cycle + sleep_interval = float(self.ping_timeout) / len(self.sockets) + + try: + # iterate over the current clients + for s in self.sockets.copy().values(): + if s.closed: + try: + del self.sockets[s.sid] + except KeyError: + # the socket could have also been removed by + # the _get_socket() method from another thread + pass + elif not s.closing: + s.check_ping_timeout() + if self.service_task_event.wait(timeout=sleep_interval): + raise KeyboardInterrupt() + except (SystemExit, KeyboardInterrupt): + self.logger.info('service task canceled') + break + except: + # an unexpected exception has occurred, log it and continue + self.logger.exception('service task exception') diff --git a/tapdown/lib/python3.11/site-packages/engineio/socket.py b/tapdown/lib/python3.11/site-packages/engineio/socket.py new file mode 100644 index 0000000..26bb94b --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/socket.py @@ -0,0 +1,256 @@ +import sys +import time + +from . import base_socket +from . import exceptions +from . import packet +from . import payload + + +class Socket(base_socket.BaseSocket): + """An Engine.IO socket.""" + def poll(self): + """Wait for packets to send to the client.""" + queue_empty = self.server.get_queue_empty_exception() + try: + packets = [self.queue.get( + timeout=self.server.ping_interval + self.server.ping_timeout)] + self.queue.task_done() + except queue_empty: + raise exceptions.QueueEmpty() + if packets == [None]: + return [] + while True: + try: + pkt = self.queue.get(block=False) + self.queue.task_done() + if pkt is None: + self.queue.put(None) + break + packets.append(pkt) + except queue_empty: + break + return packets + + def receive(self, pkt): + """Receive packet from the client.""" + packet_name = packet.packet_names[pkt.packet_type] \ + if pkt.packet_type < len(packet.packet_names) else 'UNKNOWN' + self.server.logger.info('%s: Received packet %s data %s', + self.sid, packet_name, + pkt.data if not isinstance(pkt.data, bytes) + else '') + if pkt.packet_type == packet.PONG: + self.schedule_ping() + elif pkt.packet_type == packet.MESSAGE: + self.server._trigger_event('message', self.sid, pkt.data, + run_async=self.server.async_handlers) + elif pkt.packet_type == packet.UPGRADE: + self.send(packet.Packet(packet.NOOP)) + elif pkt.packet_type == packet.CLOSE: + self.close(wait=False, abort=True, + reason=self.server.reason.CLIENT_DISCONNECT) + else: + raise exceptions.UnknownPacketError() + + def check_ping_timeout(self): + """Make sure the client is still responding to pings.""" + if self.closed: + raise exceptions.SocketIsClosedError() + if self.last_ping and \ + time.time() - self.last_ping > self.server.ping_timeout: + self.server.logger.info('%s: Client is gone, closing socket', + self.sid) + # Passing abort=False here will cause close() to write a + # CLOSE packet. This has the effect of updating half-open sockets + # to their correct state of disconnected + self.close(wait=False, abort=False, + reason=self.server.reason.PING_TIMEOUT) + return False + return True + + def send(self, pkt): + """Send a packet to the client.""" + if not self.check_ping_timeout(): + return + else: + self.queue.put(pkt) + self.server.logger.info('%s: Sending packet %s data %s', + self.sid, packet.packet_names[pkt.packet_type], + pkt.data if not isinstance(pkt.data, bytes) + else '') + + def handle_get_request(self, environ, start_response): + """Handle a long-polling GET request from the client.""" + connections = [ + s.strip() + for s in environ.get('HTTP_CONNECTION', '').lower().split(',')] + transport = environ.get('HTTP_UPGRADE', '').lower() + if 'upgrade' in connections and transport in self.upgrade_protocols: + self.server.logger.info('%s: Received request to upgrade to %s', + self.sid, transport) + return getattr(self, '_upgrade_' + transport)(environ, + start_response) + if self.upgrading or self.upgraded: + # we are upgrading to WebSocket, do not return any more packets + # through the polling endpoint + return [packet.Packet(packet.NOOP)] + try: + packets = self.poll() + except exceptions.QueueEmpty: + exc = sys.exc_info() + self.close(wait=False, reason=self.server.reason.TRANSPORT_ERROR) + raise exc[1].with_traceback(exc[2]) + return packets + + def handle_post_request(self, environ): + """Handle a long-polling POST request from the client.""" + length = int(environ.get('CONTENT_LENGTH', '0')) + if length > self.server.max_http_buffer_size: + raise exceptions.ContentTooLongError() + else: + body = environ['wsgi.input'].read(length).decode('utf-8') + p = payload.Payload(encoded_payload=body) + for pkt in p.packets: + self.receive(pkt) + + def close(self, wait=True, abort=False, reason=None): + """Close the socket connection.""" + if not self.closed and not self.closing: + self.closing = True + self.server._trigger_event( + 'disconnect', self.sid, + reason or self.server.reason.SERVER_DISCONNECT, + run_async=False) + if not abort: + self.send(packet.Packet(packet.CLOSE)) + self.closed = True + self.queue.put(None) + if wait: + self.queue.join() + + def schedule_ping(self): + self.server.start_background_task(self._send_ping) + + def _send_ping(self): + self.last_ping = None + self.server.sleep(self.server.ping_interval) + if not self.closing and not self.closed: + self.last_ping = time.time() + self.send(packet.Packet(packet.PING)) + + def _upgrade_websocket(self, environ, start_response): + """Upgrade the connection from polling to websocket.""" + if self.upgraded: + raise OSError('Socket has been upgraded already') + if self.server._async['websocket'] is None: + # the selected async mode does not support websocket + return self.server._bad_request() + ws = self.server._async['websocket']( + self._websocket_handler, self.server) + return ws(environ, start_response) + + def _websocket_handler(self, ws): + """Engine.IO handler for websocket transport.""" + def websocket_wait(): + data = ws.wait() + if data and len(data) > self.server.max_http_buffer_size: + raise ValueError('packet is too large') + return data + + # try to set a socket timeout matching the configured ping interval + # and timeout + for attr in ['_sock', 'socket']: # pragma: no cover + if hasattr(ws, attr) and hasattr(getattr(ws, attr), 'settimeout'): + getattr(ws, attr).settimeout( + self.server.ping_interval + self.server.ping_timeout) + + if self.connected: + # the socket was already connected, so this is an upgrade + self.upgrading = True # hold packet sends during the upgrade + + pkt = websocket_wait() + decoded_pkt = packet.Packet(encoded_packet=pkt) + if decoded_pkt.packet_type != packet.PING or \ + decoded_pkt.data != 'probe': + self.server.logger.info( + '%s: Failed websocket upgrade, no PING packet', self.sid) + self.upgrading = False + return [] + ws.send(packet.Packet(packet.PONG, data='probe').encode()) + self.queue.put(packet.Packet(packet.NOOP)) # end poll + + pkt = websocket_wait() + decoded_pkt = packet.Packet(encoded_packet=pkt) + if decoded_pkt.packet_type != packet.UPGRADE: + self.upgraded = False + self.server.logger.info( + ('%s: Failed websocket upgrade, expected UPGRADE packet, ' + 'received %s instead.'), + self.sid, pkt) + self.upgrading = False + return [] + self.upgraded = True + self.upgrading = False + else: + self.connected = True + self.upgraded = True + + # start separate writer thread + def writer(): + while True: + packets = None + try: + packets = self.poll() + except exceptions.QueueEmpty: + break + if not packets: + # empty packet list returned -> connection closed + break + try: + for pkt in packets: + ws.send(pkt.encode()) + except: + break + ws.close() + + writer_task = self.server.start_background_task(writer) + + self.server.logger.info( + '%s: Upgrade to websocket successful', self.sid) + + while True: + p = None + try: + p = websocket_wait() + except Exception as e: + # if the socket is already closed, we can assume this is a + # downstream error of that + if not self.closed: # pragma: no cover + self.server.logger.info( + '%s: Unexpected error "%s", closing connection', + self.sid, str(e)) + break + if p is None: + # connection closed by client + break + pkt = packet.Packet(encoded_packet=p) + try: + self.receive(pkt) + except exceptions.UnknownPacketError: # pragma: no cover + pass + except exceptions.SocketIsClosedError: # pragma: no cover + self.server.logger.info('Receive error -- socket is closed') + break + except: # pragma: no cover + # if we get an unexpected exception we log the error and exit + # the connection properly + self.server.logger.exception('Unknown receive error') + break + + self.queue.put(None) # unlock the writer task so that it can exit + writer_task.join() + self.close(wait=False, abort=True, + reason=self.server.reason.TRANSPORT_CLOSE) + + return [] diff --git a/tapdown/lib/python3.11/site-packages/engineio/static_files.py b/tapdown/lib/python3.11/site-packages/engineio/static_files.py new file mode 100644 index 0000000..77c8915 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/engineio/static_files.py @@ -0,0 +1,60 @@ +content_types = { + 'css': 'text/css', + 'gif': 'image/gif', + 'html': 'text/html', + 'jpg': 'image/jpeg', + 'js': 'application/javascript', + 'json': 'application/json', + 'png': 'image/png', + 'txt': 'text/plain', +} + + +def get_static_file(path, static_files): + """Return the local filename and content type for the requested static + file URL. + + :param path: the path portion of the requested URL. + :param static_files: a static file configuration dictionary. + + This function returns a dictionary with two keys, "filename" and + "content_type". If the requested URL does not match any static file, the + return value is None. + """ + extra_path = '' + if path in static_files: + f = static_files[path] + else: + f = None + while path != '': + path, last = path.rsplit('/', 1) + extra_path = '/' + last + extra_path + if path in static_files: + f = static_files[path] + break + elif path + '/' in static_files: + f = static_files[path + '/'] + break + if f: + if isinstance(f, str): + f = {'filename': f} + else: + f = f.copy() # in case it is mutated below + if f['filename'].endswith('/') and extra_path.startswith('/'): + extra_path = extra_path[1:] + f['filename'] += extra_path + if f['filename'].endswith('/'): + if '' in static_files: + if isinstance(static_files[''], str): + f['filename'] += static_files[''] + else: + f['filename'] += static_files['']['filename'] + if 'content_type' in static_files['']: + f['content_type'] = static_files['']['content_type'] + else: + f['filename'] += 'index.html' + if 'content_type' not in f: + ext = f['filename'].rsplit('.')[-1] + f['content_type'] = content_types.get( + ext, 'application/octet-stream') + return f diff --git a/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/INSTALLER b/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/INSTALLER new file mode 100644 index 0000000..a1b589e --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/INSTALLER @@ -0,0 +1 @@ +pip diff --git a/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/METADATA b/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/METADATA new file mode 100644 index 0000000..25ce6f6 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/RECORD b/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/RECORD new file mode 100644 index 0000000..edeadb0 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/REQUESTED b/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/REQUESTED new file mode 100644 index 0000000..e69de29 diff --git a/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/WHEEL b/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/WHEEL new file mode 100644 index 0000000..12228d4 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/AUTHORS b/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/AUTHORS new file mode 100644 index 0000000..a976907 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/LICENSE b/tapdown/lib/python3.11/site-packages/eventlet-0.40.3.dist-info/licenses/LICENSE new file mode 100644 index 0000000..2ddd0d9 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/__init__.py new file mode 100644 index 0000000..01773c5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/_version.py b/tapdown/lib/python3.11/site-packages/eventlet/_version.py new file mode 100644 index 0000000..204a16a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/asyncio.py b/tapdown/lib/python3.11/site-packages/eventlet/asyncio.py new file mode 100644 index 0000000..b9eca92 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/backdoor.py b/tapdown/lib/python3.11/site-packages/eventlet/backdoor.py new file mode 100644 index 0000000..3f3887f --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/convenience.py b/tapdown/lib/python3.11/site-packages/eventlet/convenience.py new file mode 100644 index 0000000..4d286aa --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/corolocal.py b/tapdown/lib/python3.11/site-packages/eventlet/corolocal.py new file mode 100644 index 0000000..73b10b6 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/coros.py b/tapdown/lib/python3.11/site-packages/eventlet/coros.py new file mode 100644 index 0000000..fbd7e99 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/dagpool.py b/tapdown/lib/python3.11/site-packages/eventlet/dagpool.py new file mode 100644 index 0000000..47d13a8 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/db_pool.py b/tapdown/lib/python3.11/site-packages/eventlet/db_pool.py new file mode 100644 index 0000000..7deb993 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/debug.py b/tapdown/lib/python3.11/site-packages/eventlet/debug.py new file mode 100644 index 0000000..f78e2f8 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/event.py b/tapdown/lib/python3.11/site-packages/eventlet/event.py new file mode 100644 index 0000000..122bd5d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/BaseHTTPServer.py b/tapdown/lib/python3.11/site-packages/eventlet/green/BaseHTTPServer.py new file mode 100644 index 0000000..9a73730 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/CGIHTTPServer.py b/tapdown/lib/python3.11/site-packages/eventlet/green/CGIHTTPServer.py new file mode 100644 index 0000000..285b50c --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/MySQLdb.py b/tapdown/lib/python3.11/site-packages/eventlet/green/MySQLdb.py new file mode 100644 index 0000000..16a7ec5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/SSL.py b/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/SSL.py new file mode 100644 index 0000000..bb06c8b --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/__init__.py new file mode 100644 index 0000000..1b25009 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/crypto.py b/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/crypto.py new file mode 100644 index 0000000..0a57f6f --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/crypto.py @@ -0,0 +1 @@ +from OpenSSL.crypto import * diff --git a/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/tsafe.py b/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/tsafe.py new file mode 100644 index 0000000..dd0dd8c --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/tsafe.py @@ -0,0 +1 @@ +from OpenSSL.tsafe import * diff --git a/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/version.py b/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/version.py new file mode 100644 index 0000000..c886ef0 --- /dev/null +++ b/tapdown/lib/python3.11/site-packages/eventlet/green/OpenSSL/version.py @@ -0,0 +1 @@ +from OpenSSL.version import __version__, __doc__ diff --git a/tapdown/lib/python3.11/site-packages/eventlet/green/Queue.py b/tapdown/lib/python3.11/site-packages/eventlet/green/Queue.py new file mode 100644 index 0000000..947d43a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/SimpleHTTPServer.py b/tapdown/lib/python3.11/site-packages/eventlet/green/SimpleHTTPServer.py new file mode 100644 index 0000000..df49fc9 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/SocketServer.py b/tapdown/lib/python3.11/site-packages/eventlet/green/SocketServer.py new file mode 100644 index 0000000..b94ead3 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/green/__init__.py new file mode 100644 index 0000000..d965325 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/_socket_nodns.py b/tapdown/lib/python3.11/site-packages/eventlet/green/_socket_nodns.py new file mode 100644 index 0000000..7dca20a --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/asynchat.py b/tapdown/lib/python3.11/site-packages/eventlet/green/asynchat.py new file mode 100644 index 0000000..da51396 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/asyncore.py b/tapdown/lib/python3.11/site-packages/eventlet/green/asyncore.py new file mode 100644 index 0000000..e7a7959 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/builtin.py b/tapdown/lib/python3.11/site-packages/eventlet/green/builtin.py new file mode 100644 index 0000000..ce98290 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/ftplib.py b/tapdown/lib/python3.11/site-packages/eventlet/green/ftplib.py new file mode 100644 index 0000000..b452e1d --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/http/__init__.py b/tapdown/lib/python3.11/site-packages/eventlet/green/http/__init__.py new file mode 100644 index 0000000..14e74fd --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/http/client.py b/tapdown/lib/python3.11/site-packages/eventlet/green/http/client.py new file mode 100644 index 0000000..2051ca9 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/http/cookiejar.py b/tapdown/lib/python3.11/site-packages/eventlet/green/http/cookiejar.py new file mode 100644 index 0000000..0394ca5 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/http/cookies.py b/tapdown/lib/python3.11/site-packages/eventlet/green/http/cookies.py new file mode 100644 index 0000000..d93cd71 --- /dev/null +++ b/tapdown/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/tapdown/lib/python3.11/site-packages/eventlet/green/http/server.py b/tapdown/lib/python3.11/site-packages/eventlet/green/http/server.py new file mode 100644 index 0000000..190bdb9 --- /dev/null +++ b/tapdown/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 + + +